CheckboxCheckbox components provide binary choice inputs with support for indeterminate states, validation, and grouping.

Checkbox

Checkbox components provide binary choice inputs with support for indeterminate states, validation, and grouping.

Overview

The Checkbox component allows users to select one or more options from a set. It supports controlled and uncontrolled usage patterns, validation, and accessibility features.

Key Features

  • Binary Selection: Check/uncheck states
  • Indeterminate State: For partial selections in groups
  • Validation Support: Built-in client-side validation
  • Accessibility: Full ARIA support and keyboard navigation
  • Controlled/Uncontrolled: Flexible state management
  • Form Integration: Works seamlessly with forms
  • Custom Styling: Type-safe styling with modifiers

API Reference

Checkbox

@Composable
fun Checkbox(
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit,
    modifier: Modifier = Modifier(),
    enabled: Boolean = true,
    label: String? = null,
    isIndeterminate: Boolean = false,
    validators: List<Validator> = emptyList()
)

Parameters:

  • checked: The current checked state of the checkbox
  • onCheckedChange: Callback invoked when the checkbox state changes
  • modifier: Modifier for styling and layout
  • enabled: Whether the checkbox can be interacted with (default: true)
  • label: Optional text label displayed next to the checkbox
  • isIndeterminate: Whether to show indeterminate state (default: false)
  • validators: List of validators for input validation

StatefulCheckbox

@Composable
fun StatefulCheckbox(
    initialChecked: Boolean = false,
    onCheckedChange: (Boolean) -> Unit = {},
    modifier: Modifier = Modifier(),
    enabled: Boolean = true,
    label: String? = null,
    isIndeterminate: Boolean = false,
    validators: List<Validator> = emptyList()
)

Parameters:

  • initialChecked: Initial checked state (default: false)
  • onCheckedChange: Callback invoked when state changes
  • Other parameters same as Checkbox

Usage Examples

Basic Checkbox

@Composable
fun BasicCheckboxExample() {
    var isChecked by remember { mutableStateOf(false) }

    Checkbox(
        checked = isChecked,
        onCheckedChange = { isChecked = it },
        label = "Accept terms and conditions"
    )
}

Checkbox with Validation

@Composable
fun ValidatedCheckboxExample() {
    var isAgreed by remember { mutableStateOf(false) }

    Checkbox(
        checked = isAgreed,
        onCheckedChange = { isAgreed = it },
        label = "I agree to the terms",
        validators = listOf(
            Validator { value ->
                if (value.toBoolean()) {
                    ValidationResult.valid()
                } else {
                    ValidationResult.invalid("You must agree to continue")
                }
            }
        ),
        modifier = Modifier()
            .padding(Spacing.MD)
    )
}

Checkbox Group

@Composable
fun CheckboxGroupExample() {
    val options = listOf("Option 1", "Option 2", "Option 3")
    var selectedOptions by remember { mutableStateOf(setOf<String>()) }

    Column(modifier = Modifier().gap(Spacing.SM)) {
        Text("Select options:", style = Typography.H6)

        options.forEach { option ->
            Checkbox(
                checked = option in selectedOptions,
                onCheckedChange = { isChecked ->
                    selectedOptions = if (isChecked) {
                        selectedOptions + option
                    } else {
                        selectedOptions - option
                    }
                },
                label = option,
                modifier = Modifier().padding(vertical = Spacing.XS)
            )
        }
    }
}

Indeterminate Checkbox

@Composable
fun IndeterminateCheckboxExample() {
    val childOptions = listOf("Child 1", "Child 2", "Child 3")
    var selectedChildren by remember { mutableStateOf(setOf<String>()) }

    val allSelected = selectedChildren.size == childOptions.size
    val noneSelected = selectedChildren.isEmpty()
    val isIndeterminate = !allSelected && !noneSelected

    Column(modifier = Modifier().gap(Spacing.SM)) {
        // Parent checkbox with indeterminate state
        Checkbox(
            checked = allSelected,
            onCheckedChange = { shouldSelectAll ->
                selectedChildren = if (shouldSelectAll) {
                    childOptions.toSet()
                } else {
                    emptySet()
                }
            },
            label = "Select All",
            isIndeterminate = isIndeterminate,
            modifier = Modifier()
                .padding(bottom = Spacing.SM)
                .fontWeight(FontWeight.BOLD)
        )

        // Child checkboxes
        childOptions.forEach { option ->
            Checkbox(
                checked = option in selectedChildren,
                onCheckedChange = { isChecked ->
                    selectedChildren = if (isChecked) {
                        selectedChildren + option
                    } else {
                        selectedChildren - option
                    }
                },
                label = option,
                modifier = Modifier()
                    .padding(left = Spacing.LG)
                    .padding(vertical = Spacing.XS)
            )
        }
    }
}

Stateful Checkbox

@Composable
fun StatefulCheckboxExample() {
    StatefulCheckbox(
        initialChecked = false,
        onCheckedChange = { isChecked ->
            println("Checkbox changed to: $isChecked")
        },
        label = "Enable notifications",
        modifier = Modifier()
            .padding(Spacing.MD)
    )
}

Form Integration

@Composable
fun CheckboxFormExample() {
    var newsletter by remember { mutableStateOf(false) }
    var terms by remember { mutableStateOf(false) }
    var privacy by remember { mutableStateOf(false) }

    Form(
        onSubmit = {
            println("Newsletter: $newsletter, Terms: $terms, Privacy: $privacy")
        }
    ) {
        FormField(label = "Preferences") {
            Column(modifier = Modifier().gap(Spacing.SM)) {
                Checkbox(
                    checked = newsletter,
                    onCheckedChange = { newsletter = it },
                    label = "Subscribe to newsletter"
                )

                Checkbox(
                    checked = terms,
                    onCheckedChange = { terms = it },
                    label = "Accept terms of service",
                    validators = listOf(
                        Validator { value ->
                            if (value.toBoolean()) {
                                ValidationResult.valid()
                            } else {
                                ValidationResult.invalid("Required")
                            }
                        }
                    )
                )

                Checkbox(
                    checked = privacy,
                    onCheckedChange = { privacy = it },
                    label = "Accept privacy policy",
                    validators = listOf(
                        Validator { value ->
                            if (value.toBoolean()) {
                                ValidationResult.valid()
                            } else {
                                ValidationResult.invalid("Required")
                            }
                        }
                    )
                )
            }
        }

        Button(
            text = "Submit",
            enabled = terms && privacy,
            type = ButtonType.SUBMIT
        )
    }
}

Custom Styling

@Composable
fun StyledCheckboxExample() {
    var isChecked by remember { mutableStateOf(false) }

    Checkbox(
        checked = isChecked,
        onCheckedChange = { isChecked = it },
        label = "Custom styled checkbox",
        modifier = Modifier()
            .padding(Spacing.LG)
            .backgroundColor(if (isChecked) Colors.Primary.LIGHT else Colors.Gray.LIGHT)
            .borderRadius(BorderRadius.MD)
            .border(Border.solid(2.px, Colors.Primary.MAIN))
            .padding(Spacing.MD)
    )
}

Accessibility Features

ARIA Support

The Checkbox component automatically includes:

  • role="checkbox" for screen readers
  • aria-checked state management
  • aria-describedby for validation messages
  • aria-label or aria-labelledby for labels

Keyboard Navigation

  • Space: Toggle checkbox state
  • Tab: Move focus to next interactive element
  • Shift+Tab: Move focus to previous interactive element

Screen Reader Support

@Composable
fun AccessibleCheckboxExample() {
    var isChecked by remember { mutableStateOf(false) }

    Checkbox(
        checked = isChecked,
        onCheckedChange = { isChecked = it },
        label = "Receive email notifications",
        modifier = Modifier()
            .accessibilityLabel("Toggle email notifications")
            .accessibilityHint("Double tap to ${if (isChecked) "disable" else "enable"} notifications")
    )
}

Validation Patterns

Required Checkbox

fun requiredCheckboxValidator() = Validator { value ->
    if (value.toBoolean()) {
        ValidationResult.valid()
    } else {
        ValidationResult.invalid("This field is required")
    }
}

@Composable
fun RequiredCheckboxExample() {
    var agreed by remember { mutableStateOf(false) }

    Checkbox(
        checked = agreed,
        onCheckedChange = { agreed = it },
        label = "I agree to the terms *",
        validators = listOf(requiredCheckboxValidator()),
        modifier = Modifier()
            .padding(Spacing.MD)
    )
}

Custom Validation

fun minimumSelectionValidator(minimum: Int, currentCount: Int) = Validator { _ ->
    if (currentCount >= minimum) {
        ValidationResult.valid()
    } else {
        ValidationResult.invalid("Please select at least $minimum options")
    }
}

@Composable
fun GroupValidationExample() {
    val options = listOf("Red", "Green", "Blue", "Yellow")
    var selectedColors by remember { mutableStateOf(setOf<String>()) }

    Column(modifier = Modifier().gap(Spacing.SM)) {
        Text("Choose at least 2 colors:", style = Typography.H6)

        options.forEach { color ->
            Checkbox(
                checked = color in selectedColors,
                onCheckedChange = { isChecked ->
                    selectedColors = if (isChecked) {
                        selectedColors + color
                    } else {
                        selectedColors - color
                    }
                },
                label = color,
                validators = if (color == options.last()) {
                    listOf(minimumSelectionValidator(2, selectedColors.size))
                } else {
                    emptyList()
                }
            )
        }
    }
}

State Management

Controlled Component

@Composable
fun ControlledCheckboxExample() {
    // External state management
    var checkboxState by remember { mutableStateOf(false) }

    Column {
        Checkbox(
            checked = checkboxState,
            onCheckedChange = { checkboxState = it },
            label = "Controlled checkbox"
        )

        Button(
            text = "Toggle Programmatically",
            onClick = { checkboxState = !checkboxState }
        )

        Text("Current state: $checkboxState")
    }
}

Uncontrolled Component

@Composable
fun UncontrolledCheckboxExample() {
    StatefulCheckbox(
        initialChecked = true,
        onCheckedChange = { isChecked ->
            println("Checkbox changed to: $isChecked")
        },
        label = "Uncontrolled checkbox"
    )
}

Platform Differences

Browser (JS)

  • Renders as HTML <input type="checkbox"> element
  • Native browser styling and behavior
  • Full keyboard and mouse support
  • CSS styling through modifiers

JVM

  • Server-side rendering support
  • Generates appropriate HTML for SSR
  • Maintains state during form submissions
  • Validation on server side

Performance Considerations

Optimization Tips

  1. Minimize Re-renders: Use remember for stable references
  2. Batch Updates: Group multiple checkbox changes
  3. Lazy Loading: Use lazy components for large checkbox groups
  4. Memoization: Cache expensive validator functions
@Composable
fun OptimizedCheckboxGroup() {
    val options = remember { generateLargeOptionsList() }
    var selections by remember { mutableStateOf(setOf<String>()) }

    // Memoize validator to prevent recreation
    val validator = remember {
        Validator { value ->
            // Expensive validation logic
            validateComplexRules(value)
        }
    }

    LazyColumn {
        items(options) { option ->
            Checkbox(
                checked = option in selections,
                onCheckedChange = { isChecked ->
                    selections = if (isChecked) {
                        selections + option
                    } else {
                        selections - option
                    }
                },
                label = option,
                validators = listOf(validator)
            )
        }
    }
}

Testing Strategies

Unit Testing

class CheckboxTest {
    @Test
    fun `checkbox toggles state correctly`() {
        var isChecked = false

        composeTestRule.setContent {
            Checkbox(
                checked = isChecked,
                onCheckedChange = { isChecked = it },
                label = "Test checkbox"
            )
        }

        // Click the checkbox
        composeTestRule.onNodeWithText("Test checkbox").performClick()
        assertTrue(isChecked)

        // Click again
        composeTestRule.onNodeWithText("Test checkbox").performClick()
        assertFalse(isChecked)
    }

    @Test
    fun `disabled checkbox does not respond to clicks`() {
        var isChecked = false

        composeTestRule.setContent {
            Checkbox(
                checked = isChecked,
                onCheckedChange = { isChecked = it },
                enabled = false,
                label = "Disabled checkbox"
            )
        }

        composeTestRule.onNodeWithText("Disabled checkbox").performClick()
        assertFalse(isChecked)
    }

    @Test
    fun `validation works correctly`() {
        var hasError = false

        composeTestRule.setContent {
            Checkbox(
                checked = false,
                onCheckedChange = {},
                validators = listOf(
                    Validator { value ->
                        val isValid = value.toBoolean()
                        hasError = !isValid
                        if (isValid) {
                            ValidationResult.valid()
                        } else {
                            ValidationResult.invalid("Required")
                        }
                    }
                )
            )
        }

        assertTrue(hasError)
    }
}

Integration Testing

@Test
fun `checkbox group selects all correctly`() {
    val options = listOf("A", "B", "C")
    var selectedOptions = setOf<String>()

    composeTestRule.setContent {
        CheckboxGroup(
            options = options,
            selectedOptions = selectedOptions,
            onSelectionChange = { selectedOptions = it }
        )
    }

    // Click "Select All"
    composeTestRule.onNodeWithText("Select All").performClick()
    assertEquals(options.toSet(), selectedOptions)
}

Migration Guide

From HTML Forms

<!-- Before: HTML -->
<input type="checkbox" id="newsletter" name="newsletter" value="yes">
<label for="newsletter">Subscribe to newsletter</label>
// After: Summon
@Composable
fun NewsletterCheckbox() {
    var newsletter by remember { mutableStateOf(false) }

    Checkbox(
        checked = newsletter,
        onCheckedChange = { newsletter = it },
        label = "Subscribe to newsletter"
    )
}

From Other Frameworks

// React example
const [checked, setChecked] = useState(false);
<input
    type="checkbox"
    checked={checked}
    onChange={(e) => setChecked(e.target.checked)}
/>
// Summon equivalent
var checked by remember { mutableStateOf(false) }
Checkbox(
    checked = checked,
    onCheckedChange = { checked = it }
)

Best Practices

Do

  • Use clear, descriptive labels
  • Provide validation feedback
  • Group related checkboxes logically
  • Support keyboard navigation
  • Use indeterminate state for partial selections

Don't

  • Mix checkboxes and radio buttons in the same group
  • Use very long labels that wrap multiple lines
  • Forget to handle the disabled state properly
  • Skip validation for required checkboxes
© 2025Yousef
Built withSummonSummon