# Checkbox

A control that lets people turn a single option on or off.

Checkbox is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/components/checkbox

**React**

**Example**

```tsx
import { Checkbox } from "@/components/ui/checkbox"

export default function CheckboxDemo() {
  return <Checkbox aria-label="Accept terms" />
}
```

`Checkbox` wraps the checkbox primitive and renders a check icon inside its indicator when checked. It is a single self-contained part: there are no child components to compose.

Use a checkbox for independent options that people can select in any combination, such as accepting terms or opting in to email. For a mutually exclusive choice, use a radio group; for a setting that takes effect immediately, consider a switch.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/checkbox
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add checkbox
```

### Manual

1. Install the dependencies and add the [`cn` helper](https://ui.suiss.app/docs/installation#the-cn-helper):

```bash title="Terminal"
npm install @base-ui/react @phosphor-icons/react
```

2. Copy the source into `components/ui/checkbox.tsx`:

```tsx title="components/ui/checkbox.tsx"
"use client"

import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
import { CheckIcon } from "@phosphor-icons/react"
import { cn } from "@/lib/utils"

function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
  return (
    <CheckboxPrimitive.Root
      data-slot="checkbox"
      className={cn(
        "peer relative flex size-4 shrink-0 items-center justify-center rounded-[calc(var(--radius)*0.4)] border border-foreground/25 bg-background shadow-[0_0.5px_1px_color-mix(in_oklab,var(--foreground)_12%,transparent)] outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 dark:not-data-checked:border-transparent dark:not-data-checked:bg-border dark:shadow-none data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground data-checked:shadow-none data-disabled:cursor-not-allowed data-disabled:shadow-none data-disabled:data-checked:border-transparent aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:aria-checked:border-primary group-has-focus-visible/field-label:ring-0 group-has-focus-visible/field-label:data-checked:border-primary data-disabled:opacity-50 group-has-disabled/field:opacity-50",
        className
      )}
      {...props}
    >
      <CheckboxPrimitive.Indicator
        data-slot="checkbox-indicator"
        className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
      >
        <CheckIcon />
      </CheckboxPrimitive.Indicator>
    </CheckboxPrimitive.Root>
  )
}

export { Checkbox }
```

3. Update the import paths to match your project.

## Usage

```tsx
import { Checkbox } from "@/components/ui/checkbox"
```

```tsx
<Checkbox aria-label="Accept terms" />
```

Leave the checkbox uncontrolled with `defaultChecked`, or control it with `checked` and `onCheckedChange`. Pair it with a visible `Label`: wrapping the checkbox in `Label` makes the whole label text clickable. To add supporting text, give the checkbox an `id` and an `aria-describedby` that points to the description element.

## Examples

### Checked

A checkbox that starts checked using `defaultChecked`.

**Example**

```tsx
import { Checkbox } from "@/components/ui/checkbox"

export default function CheckboxChecked() {
  return <Checkbox defaultChecked aria-label="Accept terms" />
}
```

### With label

A checkbox wrapped in a `Label`, so clicking the text toggles it.

**Example**

```tsx
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"

export default function CheckboxWithLabel() {
  return (
    <Label>
      <Checkbox />
      Accept terms and conditions
    </Label>
  )
}
```

### With description

A checkbox labelled with `htmlFor` and described by supporting text through `aria-describedby`.

**Example**

```tsx
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"

export default function CheckboxWithDescription() {
  return (
    <div className="flex w-80 items-start gap-3">
      <Checkbox
        id="marketing"
        aria-describedby="marketing-description"
        defaultChecked
        className="mt-0.75"
      />
      <div className="grid gap-1.5">
        <Label htmlFor="marketing">Product updates</Label>
        <p id="marketing-description" className="text-muted-foreground text-sm">
          Get an email when we release new features.
        </p>
      </div>
    </div>
  )
}
```

### Disabled

Disabled checkboxes, both unchecked and checked, that ignore clicks and are skipped by Tab.

**Example**

```tsx
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"

export default function CheckboxDisabled() {
  return (
    <div className="flex flex-col gap-3">
      <Label>
        <Checkbox disabled />
        Unavailable option
      </Label>
      <Label>
        <Checkbox disabled defaultChecked />
        Always included
      </Label>
    </div>
  )
}
```

### Invalid

A checkbox marked with `aria-invalid` to show a validation error.

**Example**

```tsx
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"

export default function CheckboxInvalid() {
  return (
    <Label>
      <Checkbox aria-invalid />
      You must accept the terms
    </Label>
  )
}
```

## API Reference

### Checkbox

The checkbox control, rendering the box and its check indicator.

Also accepts the props of the primitive part it wraps.

## Accessibility

- Renders with the `checkbox` role and exposes its checked state to assistive technology.
- Toggles on click and with Space when focused.
- Needs an accessible name: wrap it in a `Label`, associate a `Label` through `htmlFor` and `id`, or set `aria-label`.
- When `disabled`, it sets `aria-disabled`, ignores clicks, and is removed from the Tab order.
- Set `aria-invalid` to communicate a validation error; the border turns to the `destructive` color.

## Notes

- Styling hooks: `data-slot="checkbox"` on the root and `data-slot="checkbox-indicator"` on the indicator.
- State attributes set by the primitive drive the checked and disabled styles.
- Uses the `primary`, `primary-foreground`, `background` and `border` colors, and `opacity-50` when disabled.
- The hit area extends beyond the visible box through an `after` pseudo-element, making the small control easier to tap.

**React Native, SwiftUI, Jetpack Compose**

![Preview](https://ui.suiss.app/previews/components-checkbox--default.light.webp)

```tsx
import * as React from "react"
import { Checkbox } from "@/components/ui/checkbox"

function CheckboxDemo({ initial = false }: { initial?: boolean }) {
  const [checked, setChecked] = React.useState(initial)
  return <Checkbox checked={checked} onCheckedChange={setChecked} />
}

export function CheckboxExample() {
  return (
    <CheckboxDemo />
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CheckboxExample: View {
  var body: some View {
    CheckboxDemo(initial: false)
  }
}

struct CheckboxDemo: View {
  @State private var checked: Bool

  init(initial: Bool) {
    _checked = State(initialValue: initial)
  }

  var body: some View {
    Toggle(isOn: $checked) { EmptyView() }
      .toggleStyle(.checkbox)
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.esuiss.ui.components.Checkbox

@Composable
fun CheckboxExample() {
    CheckboxDemo(initial = false)
}

@Composable
fun CheckboxDemo(initial: Boolean) {
    var checked by rememberSaveable { mutableStateOf(initial) }
    Row {
        Checkbox(checked, { checked = it })
    }
}
```

`Checkbox` is a `Pressable` with the `checkbox` role that shows a check icon when checked and a minus icon when `indeterminate`. It has an enlarged hit area around the box so it is easy to tap.

Use it for options that can be combined, such as terms acceptance or multi-select lists. For a single on/off setting that applies right away, use Switch.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/checkbox
```

See [Installation](https://ui.suiss.app/docs/installation) for the `components.json` setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add checkbox
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add checkbox --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add checkbox --platform kotlin
```

The CLI also adds what it builds on: [Icon](https://ui.suiss.app/docs/components/icon), [Text](https://ui.suiss.app/docs/components/text).

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Icon](https://ui.suiss.app/docs/components/icon), [Text](https://ui.suiss.app/docs/components/text), and the primitives `components/primitives/checkbox.tsx`, `components/primitives/use-render.tsx`.

3. Copy the source into `components/ui/checkbox.tsx`:

```tsx title="uniwind/components/ui/checkbox.tsx"
import { cn } from "cn"
import { CheckIcon, MinusIcon } from "phosphor-react-native"
import * as CheckboxPrimitive from "@/components/primitives/checkbox"
import { resolveClassName } from "@/components/primitives/use-render"
import { Icon } from "@/components/ui/icon"

const HIT_SLOP = { top: 8, bottom: 8, left: 12, right: 12 }

function checkboxClassName(
  state: CheckboxPrimitive.RootState,
  invalid: boolean | undefined
) {
  const checked = state.checked || state.indeterminate
  return cn(
    "size-4 shrink-0 items-center justify-center rounded-[4px] border",
    checked
      ? "border-primary bg-primary"
      : cn(
          "border-foreground/25 bg-background shadow-xs dark:border-transparent dark:bg-border dark:shadow-none",
          invalid && "border-destructive dark:border-destructive"
        ),
    state.disabled ? "opacity-50" : "active:opacity-80"
  )
}

function CheckboxIcon({ className }: { className?: string }) {
  const { state } = CheckboxPrimitive.useRootContext()
  return (
    <Icon
      as={state.indeterminate ? MinusIcon : CheckIcon}
      className={cn("size-3.5 text-primary-foreground", className)}
    />
  )
}

function Checkbox({
  className,
  iconClassName,
  invalid,
  ...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root> & {
  iconClassName?: string
  invalid?: boolean
}) {
  return (
    <CheckboxPrimitive.Root
      className={(state) =>
        cn(
          checkboxClassName(state, invalid),
          resolveClassName(className, state)
        )
      }
      hitSlop={HIT_SLOP}
      {...props}
    >
      <CheckboxPrimitive.Indicator className="items-center justify-center">
        <CheckboxIcon className={iconClassName} />
      </CheckboxPrimitive.Indicator>
    </CheckboxPrimitive.Root>
  )
}

export { Checkbox }
```

4. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Icon](https://ui.suiss.app/docs/components/icon), [Text](https://ui.suiss.app/docs/components/text), and the primitives `components/primitives/checkbox.tsx`, `components/primitives/use-render.tsx`.

3. Copy the source into `components/ui/checkbox.tsx`:

```tsx title="nativewind/components/ui/checkbox.tsx"
import { cn } from "cn"
import { CheckIcon, MinusIcon } from "phosphor-react-native"
import * as CheckboxPrimitive from "@/components/primitives/checkbox"
import { resolveClassName } from "@/components/primitives/use-render"
import { Icon } from "@/components/ui/icon"

const HIT_SLOP = { top: 8, bottom: 8, left: 12, right: 12 }

function checkboxClassName(
  state: CheckboxPrimitive.RootState,
  invalid: boolean | undefined
) {
  const checked = state.checked || state.indeterminate
  return cn(
    "size-4 shrink-0 items-center justify-center rounded-[4px] border",
    checked
      ? "border-primary bg-primary"
      : cn(
          "border-foreground-25 bg-background shadow-sm dark:border-transparent dark:bg-border dark:shadow-none",
          invalid && "border-destructive dark:border-destructive"
        ),
    state.disabled ? "opacity-50" : "active:opacity-80"
  )
}

function CheckboxIcon({ className }: { className?: string }) {
  const { state } = CheckboxPrimitive.useRootContext()
  return (
    <Icon
      as={state.indeterminate ? MinusIcon : CheckIcon}
      className={cn("size-3.5 text-primary-foreground", className)}
    />
  )
}

function Checkbox({
  className,
  iconClassName,
  invalid,
  ...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root> & {
  iconClassName?: string
  invalid?: boolean
}) {
  return (
    <CheckboxPrimitive.Root
      className={(state) =>
        cn(
          checkboxClassName(state, invalid),
          resolveClassName(className, state)
        )
      }
      hitSlop={HIT_SLOP}
      {...props}
    >
      <CheckboxPrimitive.Indicator className="items-center justify-center">
        <CheckboxIcon className={iconClassName} />
      </CheckboxPrimitive.Indicator>
    </CheckboxPrimitive.Root>
  )
}

export { Checkbox }
```

4. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

```bash title="Terminal"
npx expo install phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Icon](https://ui.suiss.app/docs/components/icon), [Text](https://ui.suiss.app/docs/components/text), and the primitives `components/primitives/checkbox.tsx`, `components/primitives/use-render.tsx`.

3. Copy the source into `components/ui/checkbox.tsx`:

```tsx title="stylesheet/components/ui/checkbox.tsx"
import { CheckIcon, MinusIcon } from "phosphor-react-native"
import {
  Pressable,
  type StyleProp,
  StyleSheet,
  type TextStyle,
  type ViewStyle,
} from "react-native"
import * as CheckboxPrimitive from "@/components/primitives/checkbox"
import { resolveStyle } from "@/components/primitives/use-render"
import { Icon } from "@/components/ui/icon"
import { TextStyleContext } from "@/components/ui/text"
import { shadow, type ThemeColors, useTheme } from "@/components/ui/theme"

const HIT_SLOP = { top: 8, bottom: 8, left: 12, right: 12 }

const styles = StyleSheet.create({
  root: {
    width: 16,
    height: 16,
    flexShrink: 0,
    alignItems: "center",
    justifyContent: "center",
    borderRadius: 4,
    borderWidth: 1,
  },
  indicator: { alignItems: "center", justifyContent: "center" },
})

type SelectionState = {
  checked: boolean
  disabled: boolean
}

function selectionControlStyle(
  colors: ThemeColors,
  isDark: boolean,
  state: SelectionState,
  pressed: boolean,
  invalid: boolean | undefined
): ViewStyle {
  let opacity = 1
  if (pressed) opacity = 0.8
  if (state.disabled) opacity = 0.5
  if (state.checked) {
    return {
      borderColor: colors.primary,
      backgroundColor: colors.primary,
      opacity,
    }
  }
  if (isDark) {
    return {
      borderColor: invalid ? colors.destructive : "transparent",
      backgroundColor: colors.border,
      opacity,
    }
  }
  return {
    borderColor: invalid ? colors.destructive : `${colors.foreground}40`,
    backgroundColor: colors.background,
    opacity,
    ...shadow.xs,
  }
}

function CheckboxIcon({ iconStyle }: { iconStyle?: StyleProp<TextStyle> }) {
  const { colors } = useTheme()
  const { state } = CheckboxPrimitive.useRootContext()
  return (
    <TextStyleContext.Provider
      value={
        StyleSheet.flatten([{ color: colors.primaryForeground }, iconStyle]) ??
        undefined
      }
    >
      <Icon as={state.indeterminate ? MinusIcon : CheckIcon} size={14} />
    </TextStyleContext.Provider>
  )
}

function Checkbox({
  style,
  iconStyle,
  invalid,
  ...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root> & {
  iconStyle?: StyleProp<TextStyle>
  invalid?: boolean
}) {
  const { colors, isDark } = useTheme()
  return (
    <CheckboxPrimitive.Root
      hitSlop={HIT_SLOP}
      {...props}
      render={(renderProps, state) => (
        <Pressable
          {...renderProps}
          style={({ pressed }) => [
            styles.root,
            selectionControlStyle(
              colors,
              isDark,
              {
                checked: state.checked || state.indeterminate,
                disabled: state.disabled,
              },
              pressed,
              invalid
            ),
            resolveStyle(style, state) as StyleProp<ViewStyle>,
          ]}
        />
      )}
    >
      <CheckboxPrimitive.Indicator style={styles.indicator}>
        <CheckboxIcon iconStyle={iconStyle} />
      </CheckboxPrimitive.Indicator>
    </CheckboxPrimitive.Root>
  )
}

export { Checkbox }
```

4. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Add the Swift package to your app:

```swift title="Package.swift"
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
```

3. Copy the theme files every view uses:

- `swift/Theme/Theme.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Theme.swift)

- `swift/Theme/Tokens.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Tokens.swift)

4. Copy the source and every file it uses:

```swift title="swift/Components/Checkbox.swift"
import PhosphorSwift
import SwiftUI

extension Shadow {
  static let control = Shadow(color: .black.opacity(0.08), radius: 0.5, y: 0.5)
  static let thumb = Shadow(color: .black.opacity(0.12), radius: 1.5, y: 1)
}

struct SelectionIndicator<Mark: View>: View {
  let checked: Bool
  let pressed: Bool
  let cornerRadius: CGFloat
  let mark: Mark

  @Environment(\.isEnabled) private var isEnabled
  @Environment(\.isInvalid) private var invalid
  @Environment(\.colorScheme) private var colorScheme

  init(checked: Bool, pressed: Bool, cornerRadius: CGFloat, @ViewBuilder mark: () -> Mark) {
    self.checked = checked
    self.pressed = pressed
    self.cornerRadius = cornerRadius
    self.mark = mark()
  }

  var body: some View {
    let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
    ZStack {
      shape.fill(fill)
      shape.strokeBorder(border, lineWidth: 1)
      if checked {
        mark
      }
    }
    .frame(width: 16, height: 16)
    .shadow(raised ? .control : Shadow(color: .clear, radius: 0))
    .opacity(isEnabled ? (pressed ? 0.8 : 1) : 0.5)
  }

  private var isDark: Bool { colorScheme == .dark }

  private var raised: Bool { !checked && isEnabled && !isDark }

  private var fill: Color {
    if checked { return EsuissColor.primary }
    return isDark ? EsuissColor.border : EsuissColor.background
  }

  private var border: Color {
    if checked { return EsuissColor.primary }
    if invalid && isEnabled { return EsuissColor.destructive }
    return isDark ? .clear : EsuissColor.foreground.opacity(0.25)
  }
}

struct PressReader<Content: View>: ButtonStyle {
  let content: (Bool) -> Content

  init(@ViewBuilder content: @escaping (Bool) -> Content) {
    self.content = content
  }

  func makeBody(configuration: Configuration) -> some View {
    content(configuration.isPressed)
  }
}

public struct CheckboxStyle: ToggleStyle {
  public init() {}

  public func makeBody(configuration: Configuration) -> some View {
    HapticButton(style: PressReader { pressed in
      HStack(spacing: 12) {
        SelectionIndicator(
          checked: configuration.isOn || configuration.isMixed,
          pressed: pressed,
          cornerRadius: 4
        ) {
          (configuration.isMixed ? Ph.minus.regular : Ph.check.regular)
            .frame(width: 14, height: 14)
            .foregroundStyle(EsuissColor.primaryForeground)
        }
        configuration.label
      }
      .contentShape(Rectangle().inset(by: -8))
    }) {
      configuration.isOn.toggle()
      return true
    }
    .accessibilityElement(children: .combine)
    .accessibilityRemoveTraits(.isButton)
    .accessibilityAddTraits(.isToggle)
    .accessibilityValue(
      configuration.isMixed
        ? Text("Mixed")
        : configuration.isOn ? Text("Checked") : Text("Unchecked")
    )
  }
}

extension ToggleStyle where Self == CheckboxStyle {
  public static var checkbox: CheckboxStyle { CheckboxStyle() }
}
```

- `swift/Components/Field.swift`: see [Field](https://ui.suiss.app/docs/components/field.md)

- `swift/Components/Haptics.swift`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `swift/Components/Separator.swift`: see [Separator](https://ui.suiss.app/docs/components/separator.md)

- `swift/Components/TextVariant.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/TextVariant.swift)

5. Add the files to your app target.

**Jetpack Compose**

1. Set `minSdk` to 24 or higher and add the dependencies:

```kotlin title="build.gradle.kts"
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
```

2. Copy the theme files every view uses:

- `kotlin/theme/Theme.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Theme.kt)

- `kotlin/theme/Tokens.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Tokens.kt)

3. Copy the source and every file it uses:

```kt title="kotlin/components/Checkbox.kt"
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.triStateToggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.error
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Check
import com.adamglin.phosphoricons.regular.Minus
import com.esuiss.ui.theme.Colors
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.shadow

@Composable
fun Checkbox(
    checked: Boolean,
    onCheckedChange: ((Boolean) -> Unit)?,
    modifier: Modifier = Modifier,
    enabled: Boolean = LocalFieldState.current.enabled,
    invalid: Boolean = LocalFieldState.current.invalid,
    interactionSource: MutableInteractionSource? = null,
) {
    TriStateCheckbox(
        state = ToggleableState(checked),
        onClick = onCheckedChange?.let { { it(!checked) } },
        modifier = modifier,
        enabled = enabled,
        invalid = invalid,
        interactionSource = interactionSource,
    )
}

@Composable
fun TriStateCheckbox(
    state: ToggleableState,
    onClick: (() -> Unit)?,
    modifier: Modifier = Modifier,
    enabled: Boolean = LocalFieldState.current.enabled,
    invalid: Boolean = LocalFieldState.current.invalid,
    interactionSource: MutableInteractionSource? = null,
) {
    val colors = EsuissTheme.colors
    val haptics = rememberHaptics()
    val source = interactionSource ?: remember { MutableInteractionSource() }
    val pressed by source.collectIsPressedAsState()
    val on = state != ToggleableState.Off
    val shape = RoundedCornerShape(4.dp)
    val look = selectionControlLook(colors, on, enabled, invalid, pressed)
    val toggle = if (onClick != null) {
        Modifier
            .triStateToggleable(
                state = state,
                interactionSource = source,
                indication = null,
                enabled = enabled,
                role = Role.Checkbox,
                onClick = {
                    haptics.toggle(state != ToggleableState.On)
                    onClick()
                },
            )
            .formTouchTarget()
    } else {
        Modifier
    }
    Box(
        modifier = modifier
            .then(toggle)
            .then(if (invalid) Modifier.semantics { error("Invalid") } else Modifier)
            .size(16.dp)
            .alpha(look.alpha)
            .then(if (look.shadow) Modifier.shadow(FormShadowXs, shape) else Modifier)
            .background(look.background, shape)
            .border(1.dp, look.border, shape),
        contentAlignment = Alignment.Center,
    ) {
        if (on) {
            Icon(
                if (state == ToggleableState.Indeterminate) PhosphorIcons.Regular.Minus else PhosphorIcons.Regular.Check,
                contentDescription = null,
                size = 14.dp,
                tint = colors.primaryForeground,
            )
        }
    }
}

internal class SelectionControlLook(val background: Color, val border: Color, val shadow: Boolean, val alpha: Float)

internal fun selectionControlLook(
    colors: Colors,
    on: Boolean,
    enabled: Boolean,
    invalid: Boolean,
    pressed: Boolean,
): SelectionControlLook {
    val alpha = when {
        !enabled -> 0.5f
        pressed -> 0.8f
        else -> 1f
    }
    return when {
        on -> SelectionControlLook(colors.primary, colors.primary, false, alpha)
        colors.isDark -> SelectionControlLook(
            colors.border,
            if (invalid) colors.destructive else Color.Transparent,
            false,
            alpha,
        )
        else -> SelectionControlLook(
            colors.background,
            if (invalid) colors.destructive else colors.foreground.copy(alpha = 0.25f),
            enabled,
            alpha,
        )
    }
}
```

- `kotlin/components/Content.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Content.kt)

- `kotlin/components/Field.kt`: see [Field](https://ui.suiss.app/docs/components/field.md)

- `kotlin/components/FormSupport.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/FormSupport.kt)

- `kotlin/components/Haptics.kt`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `kotlin/components/Icon.kt`: see [Icon](https://ui.suiss.app/docs/components/icon.md)

- `kotlin/components/Label.kt`: see [Label](https://ui.suiss.app/docs/components/label.md)

- `kotlin/components/Separator.kt`: see [Separator](https://ui.suiss.app/docs/components/separator.md)

4. Update the `com.esuiss.ui` package names to match your project.

## Usage

```tsx
import { Checkbox } from "@/components/ui/checkbox"
```

```tsx
<CheckboxDemo />
```

Use `checked` with `onCheckedChange` for controlled state, or `defaultChecked` for uncontrolled. Set `indeterminate` for a parent checkbox whose children are partly selected.

Pair the checkbox with a `Label`: give the label a `nativeID` and the checkbox a matching `accessibilityLabelledBy`, and toggle the value from the label's `onPress`. Set `invalid` to show a destructive border.

## Examples

### Checked

Starts checked.

![Preview](https://ui.suiss.app/previews/components-checkbox--checked.light.webp)

```tsx
import * as React from "react"
import { Checkbox } from "@/components/ui/checkbox"

function CheckboxDemo({ initial = false }: { initial?: boolean }) {
  const [checked, setChecked] = React.useState(initial)
  return <Checkbox checked={checked} onCheckedChange={setChecked} />
}

export function CheckboxCheckedExample() {
  return (
    <CheckboxDemo initial />
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CheckboxCheckedExample: View {
  var body: some View {
    CheckboxDemo(initial: true)
  }
}

struct CheckboxDemo: View {
  @State private var checked: Bool

  init(initial: Bool) {
    _checked = State(initialValue: initial)
  }

  var body: some View {
    Toggle(isOn: $checked) { EmptyView() }
      .toggleStyle(.checkbox)
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.esuiss.ui.components.Checkbox

@Composable
fun CheckboxCheckedExample() {
    CheckboxDemo(initial = true)
}

@Composable
fun CheckboxDemo(initial: Boolean) {
    var checked by rememberSaveable { mutableStateOf(initial) }
    Row {
        Checkbox(checked, { checked = it })
    }
}
```

### With label

Linked to a `Label` that also toggles it.

![Preview](https://ui.suiss.app/previews/components-checkbox--with-label.light.webp)

```tsx
import * as React from "react"
import { View } from "react-native"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"

function WithLabelDemo() {
  const [checked, setChecked] = React.useState(false)
  return (
    <View className="flex-row items-center gap-3">
      <Checkbox
        accessibilityLabelledBy="terms"
        checked={checked}
        onCheckedChange={setChecked}
      />
      <Label nativeID="terms" onPress={() => setChecked((value) => !value)}>
        Accept terms and conditions
      </Label>
    </View>
  )
}

export function CheckboxWithLabelExample() {
  return (
    <WithLabelDemo />
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CheckboxWithLabelDemo: View {
  @State private var checked = false

  var body: some View {
    Toggle(isOn: $checked) {
      FieldLabel("Accept terms and conditions")
    }
    .toggleStyle(.checkbox)
  }
}
```

```kotlin
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Checkbox
import com.esuiss.ui.components.Label

fun Modifier.checkboxLabelToggle(onToggle: () -> Unit): Modifier =
    clickable(interactionSource = null, indication = null, onClick = onToggle)

@Composable
fun CheckboxWithLabel() {
    var checked by rememberSaveable { mutableStateOf(false) }
    Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
        Checkbox(checked, { checked = it }, Modifier.semantics { contentDescription = "Accept terms and conditions" })
        Label("Accept terms and conditions", Modifier.checkboxLabelToggle { checked = !checked })
    }
}
```

### With description

A label with a supporting description.

![Preview](https://ui.suiss.app/previews/components-checkbox--with-description.light.webp)

```tsx
import * as React from "react"
import { View } from "react-native"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
import { Text } from "@/components/ui/text"

function WithDescriptionDemo() {
  const [checked, setChecked] = React.useState(true)
  return (
    <View className="flex-row items-start gap-3">
      <Checkbox
        accessibilityLabelledBy="notifications"
        checked={checked}
        onCheckedChange={setChecked}
      />
      <View className="flex-1 gap-1.5">
        <Label
          nativeID="notifications"
          onPress={() => setChecked((value) => !value)}
        >
          Enable notifications
        </Label>
        <Text className="text-sm text-muted-foreground">
          You can enable or disable notifications at any time.
        </Text>
      </View>
    </View>
  )
}

export function CheckboxWithDescriptionExample() {
  return (
    <WithDescriptionDemo />
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CheckboxWithDescriptionDemo: View {
  @State private var checked = true

  var body: some View {
    HStack(alignment: .top, spacing: 12) {
      Toggle(isOn: $checked) { EmptyView() }
        .toggleStyle(.checkbox)
        .accessibilityLabel(Text("Enable notifications"))
        .padding(.top, 2)
      VStack(alignment: .leading, spacing: 6) {
        FieldLabel("Enable notifications")
          .onTapGesture { checked.toggle() }
        Text("You can enable or disable notifications at any time.")
          .textStyle(.sm)
          .foregroundStyle(EsuissColor.mutedForeground)
      }
      .frame(maxWidth: .infinity, alignment: .leading)
    }
  }
}
```

```kotlin
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Checkbox
import com.esuiss.ui.components.Label
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

fun Modifier.checkboxLabelToggle(onToggle: () -> Unit): Modifier =
    clickable(interactionSource = null, indication = null, onClick = onToggle)

@Composable
fun CheckboxWithDescription() {
    var checked by rememberSaveable { mutableStateOf(true) }
    Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.Top) {
        Checkbox(checked, { checked = it }, Modifier.semantics { contentDescription = "Enable notifications" })
        Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) {
            Label("Enable notifications", Modifier.checkboxLabelToggle { checked = !checked })
            Text(
                "You can enable or disable notifications at any time.",
                style = Typography.sm.copy(color = EsuissTheme.colors.mutedForeground),
            )
        }
    }
}
```

### Disabled

Disabled checkboxes in both states.

![Preview](https://ui.suiss.app/previews/components-checkbox--disabled.light.webp)

```tsx
import { View } from "react-native"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"

export function CheckboxDisabledExample() {
  return (
    <View className="gap-4">
      <View className="flex-row items-center gap-3">
        <Checkbox disabled checked={false} onCheckedChange={() => undefined} />
        <Label disabled>Unchecked</Label>
      </View>
      <View className="flex-row items-center gap-3">
        <Checkbox disabled checked onCheckedChange={() => undefined} />
        <Label disabled>Checked</Label>
      </View>
    </View>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CheckboxDisabledExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 16) {
      Toggle(isOn: .constant(false)) { FieldLabel("Unchecked") }
      Toggle(isOn: .constant(true)) { FieldLabel("Checked") }
    }
    .toggleStyle(.checkbox)
    .disabled(true)
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Checkbox
import com.esuiss.ui.components.Label

@Composable
fun CheckboxDisabled() {
    Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
        Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
            Checkbox(false, {}, enabled = false)
            Label("Unchecked", enabled = false)
        }
        Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
            Checkbox(true, {}, enabled = false)
            Label("Checked", enabled = false)
        }
    }
}
```

### Indeterminate

A select-all checkbox in the indeterminate state.

![Preview](https://ui.suiss.app/previews/components-checkbox--indeterminate.light.webp)

```tsx
import { View } from "react-native"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"

export function CheckboxIndeterminateExample() {
  return (
    <View className="flex-row items-center gap-3">
      <Checkbox
        indeterminate
        checked={false}
        onCheckedChange={() => undefined}
        accessibilityLabelledBy="select-all"
      />
      <Label nativeID="select-all">Select all</Label>
    </View>
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CheckboxIndeterminateExample: View {
  var body: some View {
    Toggle(sources: [Binding.constant(true), .constant(false)], isOn: \.self) {
      FieldLabel("Select all")
    }
    .toggleStyle(.checkbox)
  }
}
```

```kotlin
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Label
import com.esuiss.ui.components.TriStateCheckbox

fun Modifier.checkboxLabelToggle(onToggle: () -> Unit): Modifier =
    clickable(interactionSource = null, indication = null, onClick = onToggle)

@Composable
fun CheckboxIndeterminate() {
    Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
        TriStateCheckbox(ToggleableState.Indeterminate, {}, Modifier.semantics { contentDescription = "Select all" })
        Label("Select all")
    }
}
```

### Invalid

A required checkbox with the `invalid` border.

![Preview](https://ui.suiss.app/previews/components-checkbox--invalid.light.webp)

```tsx
import * as React from "react"
import { View } from "react-native"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"

function InvalidDemo() {
  const [checked, setChecked] = React.useState(false)
  return (
    <View className="flex-row items-center gap-3">
      <Checkbox
        invalid={!checked}
        accessibilityLabelledBy="terms-invalid"
        checked={checked}
        onCheckedChange={setChecked}
      />
      <Label
        nativeID="terms-invalid"
        onPress={() => setChecked((value) => !value)}
      >
        Accept terms and conditions
      </Label>
    </View>
  )
}

export function CheckboxInvalidExample() {
  return (
    <InvalidDemo />
  )
}
```

```swift
import EsuissUI
import SwiftUI

struct CheckboxInvalidDemo: View {
  @State private var checked = false

  var body: some View {
    Toggle(isOn: $checked) {
      FieldLabel("Accept terms and conditions")
    }
    .toggleStyle(.checkbox)
    .invalid(!checked)
  }
}
```

```kotlin
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Checkbox
import com.esuiss.ui.components.Label

fun Modifier.checkboxLabelToggle(onToggle: () -> Unit): Modifier =
    clickable(interactionSource = null, indication = null, onClick = onToggle)

@Composable
fun CheckboxInvalid() {
    var checked by rememberSaveable { mutableStateOf(false) }
    Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
        Checkbox(
            checked,
            { checked = it },
            Modifier.semantics { contentDescription = "Accept terms and conditions" },
            invalid = !checked,
        )
        Label("Accept terms and conditions", Modifier.checkboxLabelToggle { checked = !checked })
    }
}
```

## API Reference

### Checkbox

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `iconClassName` | `string` | - | Extra classes for the check icon. |
| `invalid` | `boolean` | - | Shows a destructive border. |

Also accepts the props of `React.ComponentProps<typeof CheckboxPrimitive.Root>`.

## Accessibility

- Has the `checkbox` role with `checked` (or `mixed` when indeterminate) and `disabled` state.
- Link it to a visible label with `accessibilityLabelledBy`, or give it an `accessibilityLabel`.

## Notes

- Uses `bg-primary` when checked and `bg-background` with a translucent border when unchecked; in dark mode the unchecked box is filled instead of outlined.
- Light and dark follow the system appearance.

