# Toggle

A two-state button that stays pressed until it's pressed again.

Toggle 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/toggle

**React**

**Example**

```tsx
import { TextBIcon } from "@phosphor-icons/react"
import { Toggle } from "@/components/ui/toggle"

export default function ToggleDemo() {
  return (
    <Toggle aria-label="Toggle bold">
      <TextBIcon />
    </Toggle>
  )
}
```

`Toggle` wraps the toggle primitive of the chosen base. When pressed, it shows a tinted background and `primary`-colored content, and it scales down slightly while being pressed.

Use a toggle for a single on-off option such as bold text or a bookmark. To choose among related options, use `ToggleGroup`; for a setting that takes effect immediately, use `Switch`.

## Installation

### CLI

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

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 toggle
```

### 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 class-variance-authority
```

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

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

import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const toggleVariants = cva(
  "inline-flex items-center justify-center gap-1 rounded-lg text-sm whitespace-nowrap transition-[color,background-color,scale] duration-200 ease-[cubic-bezier(0.3,1.25,0.5,1)] outline-none active:scale-96 motion-reduce:active:scale-100 hover:bg-accent hover:text-accent-foreground focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-pressed:bg-accent aria-pressed:text-primary aria-pressed:hover:bg-accent aria-pressed:hover:text-primary [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-transparent",
        outline:
          "border border-border bg-transparent hover:bg-accent hover:text-accent-foreground",
      },
      size: {
        default:
          "h-9 min-w-9 px-3 has-data-[icon=inline-end]:pe-2.5 has-data-[icon=inline-start]:ps-2.5",
        sm: "h-8 min-w-8 px-3 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
        lg: "h-10 min-w-10 px-4 has-data-[icon=inline-end]:pe-3 has-data-[icon=inline-start]:ps-3",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

function Toggle({
  className,
  variant,
  size,
  ...props
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
  return (
    <TogglePrimitive
      data-slot="toggle"
      className={cn(toggleVariants({ variant, size, className }))}
      {...props}
    />
  )
}

export { Toggle, toggleVariants }
```

3. Update the import paths to match your project.

## Usage

```tsx
import { Toggle } from "@/components/ui/toggle"
```

```tsx
<Toggle aria-label="Toggle bold"><TextBIcon /></Toggle>
```

Use `defaultPressed` for an uncontrolled toggle, or `pressed` with `onPressedChange` to control it. Mark an icon beside a label with `data-icon="inline-start"` or `data-icon="inline-end"` to tighten the padding on that side.

## Examples

### Pressed

Starts in the pressed state.

**Example**

```tsx
import { TextBIcon } from "@phosphor-icons/react"
import { Toggle } from "@/components/ui/toggle"

export default function TogglePressed() {
  return (
    <Toggle aria-label="Toggle bold" defaultPressed>
      <TextBIcon />
    </Toggle>
  )
}
```

### Variants

Compares the `default` and `outline` variants.

**Example**

```tsx
import { TextBIcon } from "@phosphor-icons/react"
import { Toggle } from "@/components/ui/toggle"

export default function ToggleVariants() {
  return (
    <div className="flex items-center gap-3">
      <Toggle aria-label="Toggle bold" variant="default">
        <TextBIcon />
      </Toggle>
      <Toggle aria-label="Toggle bold" variant="outline">
        <TextBIcon />
      </Toggle>
    </div>
  )
}
```

### Sizes

Compares the `sm`, `default` and `lg` sizes.

**Example**

```tsx
import { TextBIcon } from "@phosphor-icons/react"
import { Toggle } from "@/components/ui/toggle"

export default function ToggleSizes() {
  return (
    <div className="flex items-center gap-3">
      <Toggle aria-label="Toggle bold" size="sm">
        <TextBIcon />
      </Toggle>
      <Toggle aria-label="Toggle bold" size="default">
        <TextBIcon />
      </Toggle>
      <Toggle aria-label="Toggle bold" size="lg">
        <TextBIcon />
      </Toggle>
    </div>
  )
}
```

### With text

Pairs an icon with a text label.

**Example**

```tsx
import { BookmarkSimpleIcon, TextItalicIcon } from "@phosphor-icons/react"
import { Toggle } from "@/components/ui/toggle"

export default function ToggleWithText() {
  return (
    <div className="flex items-center gap-3">
      <Toggle aria-label="Toggle italic">
        <TextItalicIcon data-icon="inline-start" />
        Italic
      </Toggle>
      <Toggle variant="outline" aria-label="Save for later">
        <BookmarkSimpleIcon data-icon="inline-start" />
        Save
      </Toggle>
    </div>
  )
}
```

### Disabled

Disables the toggle so it ignores clicks.

**Example**

```tsx
import { TextBIcon } from "@phosphor-icons/react"
import { Toggle } from "@/components/ui/toggle"

export default function ToggleDisabled() {
  return (
    <Toggle disabled aria-label="Toggle bold">
      <TextBIcon />
    </Toggle>
  )
}
```

## API Reference

### Toggle

The pressable two-state button.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "outline"` | `"default"` | Uses a borderless `default` style or an `outline` style with a border. |
| `size` | `"default" \| "sm" \| "lg"` | `"default"` | Sets the height and padding: `sm`, `default` or `lg`. |

Also accepts the props of the primitive part it wraps.

## Accessibility

- Renders a `button` with `aria-pressed` reflecting its state.
- `Enter` and `Space` toggle the focused button.
- An icon-only toggle needs an `aria-label`.

## Notes

- Uses `data-slot="toggle"`. Pressed styles key off `aria-pressed`.
- `toggleVariants` is exported so other components, such as `ToggleGroupItem`, can share the styles.
- The press animation is removed when reduced motion is preferred.

**React Native, SwiftUI, Jetpack Compose**

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

```tsx
import { TextBIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Toggle, ToggleIcon } from "@/components/ui/toggle"

type ToggleDemoProps = Omit<
  React.ComponentProps<typeof Toggle>,
  "pressed" | "onPressedChange"
> & {
  initial?: boolean
}

function ToggleDemo({ initial = false, children, ...props }: ToggleDemoProps) {
  const [pressed, setPressed] = React.useState(initial)
  return (
    <Toggle pressed={pressed} onPressedChange={setPressed} {...props}>
      {children}
    </Toggle>
  )
}

export function ToggleExample() {
  return (
    <View className="flex-row">
      <ToggleDemo accessibilityLabel="Toggle bold">
        <ToggleIcon as={TextBIcon} />
      </ToggleDemo>
    </View>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ToggleExample: View {
  var body: some View {
    ToggleDemo(label: "Toggle bold") { Ph.textB.regular }
  }
}

struct ToggleDemo<Icon: View>: View {
  let variant: ToggleVariant
  let size: ToggleSize
  let label: LocalizedStringKey
  let icon: Icon

  @State private var isOn: Bool

  init(
    initial: Bool = false,
    variant: ToggleVariant = .default,
    size: ToggleSize = .default,
    label: LocalizedStringKey,
    @ViewBuilder icon: () -> Icon
  ) {
    self.variant = variant
    self.size = size
    self.label = label
    self.icon = icon()
    _isOn = State(initialValue: initial)
  }

  var body: some View {
    Toggle(isOn: $isOn) {
      icon.frame(width: 16, height: 16)
    }
    .toggleStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
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.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.TextB
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Toggle
import com.esuiss.ui.components.ToggleSize
import com.esuiss.ui.components.ToggleVariant

@Composable
fun ToggleDemo(
    label: String,
    initial: Boolean = false,
    variant: ToggleVariant = ToggleVariant.Default,
    size: ToggleSize = ToggleSize.Default,
    enabled: Boolean = true,
    content: @Composable RowScope.() -> Unit,
) {
    var pressed by rememberSaveable { mutableStateOf(initial) }
    Toggle(
        pressed,
        { pressed = it },
        Modifier.semantics { contentDescription = label },
        variant = variant,
        size = size,
        enabled = enabled,
        content = content,
    )
}

@Composable
fun ToggleRow(content: @Composable () -> Unit) {
    Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
        content()
    }
}

@Composable
fun ToggleBold(initial: Boolean = false) {
    ToggleRow {
        ToggleDemo("Toggle bold", initial = initial) {
            Icon(PhosphorIcons.Regular.TextB, contentDescription = null)
        }
    }
}
```

`Toggle` is a `Pressable` with an on and off state. When on, it shows a selected background and its text turns to the primary color; it scales down slightly while pressed. It has two variants, `default` and `outline`, and three sizes.

`ToggleIcon` is an `Icon` sized for use inside a toggle.

## Installation

**React Native**

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

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 toggle
```

**SwiftUI**

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

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add toggle --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 class-variance-authority cn
```

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/toggle.tsx`, `components/primitives/use-render.tsx`.

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

```tsx title="uniwind/components/ui/toggle.tsx"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import { Pressable } from "react-native"
import {
  Toggle as TogglePrimitive,
  type ToggleState,
} from "@/components/primitives/toggle"
import { resolveClassName } from "@/components/primitives/use-render"
import { Icon } from "@/components/ui/icon"
import { TextClassContext } from "@/components/ui/text"

const toggleVariants = cva(
  "flex-row items-center justify-center gap-1 rounded-lg",
  {
    variants: {
      variant: {
        default: "bg-transparent",
        outline: "border border-border bg-transparent",
      },
      size: {
        default: "h-9 min-w-9 px-3",
        sm: "h-8 min-w-8 px-3",
        lg: "h-10 min-w-10 px-4",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

function toggleStateClassName(state: ToggleState) {
  return cn(
    state.pressed && "bg-accent",
    state.disabled ? "opacity-50" : "active:scale-96 active:opacity-80"
  )
}

function toggleTextClassName(state: ToggleState) {
  return cn("text-sm text-foreground", state.pressed && "text-primary")
}

function Toggle({
  className,
  variant,
  size,
  ...props
}: React.ComponentProps<typeof TogglePrimitive> &
  VariantProps<typeof toggleVariants>) {
  return (
    <TogglePrimitive
      className={(state) =>
        cn(
          toggleVariants({ variant, size }),
          toggleStateClassName(state),
          resolveClassName(className, state)
        )
      }
      render={(renderProps, state) => (
        <TextClassContext.Provider value={toggleTextClassName(state)}>
          <Pressable {...renderProps} />
        </TextClassContext.Provider>
      )}
      {...props}
    />
  )
}

function ToggleIcon({
  className,
  ...props
}: React.ComponentProps<typeof Icon>) {
  return <Icon className={cn("size-4 shrink-0", className)} {...props} />
}

export {
  Toggle,
  ToggleIcon,
  toggleStateClassName,
  toggleTextClassName,
  toggleVariants,
}
```

4. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install class-variance-authority cn
```

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/toggle.tsx`, `components/primitives/use-render.tsx`.

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

```tsx title="nativewind/components/ui/toggle.tsx"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import { Pressable } from "react-native"
import {
  Toggle as TogglePrimitive,
  type ToggleState,
} from "@/components/primitives/toggle"
import { resolveClassName } from "@/components/primitives/use-render"
import { Icon } from "@/components/ui/icon"
import { TextClassContext } from "@/components/ui/text"

const toggleVariants = cva(
  "flex-row items-center justify-center gap-1 rounded-lg",
  {
    variants: {
      variant: {
        default: "bg-transparent",
        outline: "border border-border bg-transparent",
      },
      size: {
        default: "h-9 min-w-9 px-3",
        sm: "h-8 min-w-8 px-3",
        lg: "h-10 min-w-10 px-4",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

function toggleStateClassName(state: ToggleState) {
  return cn(
    state.pressed && "bg-accent",
    state.disabled ? "opacity-50" : "active:scale-[0.96] active:opacity-80"
  )
}

function toggleTextClassName(state: ToggleState) {
  return cn("text-sm text-foreground", state.pressed && "text-primary")
}

function Toggle({
  className,
  variant,
  size,
  ...props
}: React.ComponentProps<typeof TogglePrimitive> &
  VariantProps<typeof toggleVariants>) {
  return (
    <TogglePrimitive
      className={(state) =>
        cn(
          toggleVariants({ variant, size }),
          toggleStateClassName(state),
          resolveClassName(className, state)
        )
      }
      render={(renderProps, state) => (
        <TextClassContext.Provider value={toggleTextClassName(state)}>
          <Pressable {...renderProps} />
        </TextClassContext.Provider>
      )}
      {...props}
    />
  )
}

function ToggleIcon({
  className,
  ...props
}: React.ComponentProps<typeof Icon>) {
  return <Icon className={cn("size-4 shrink-0", className)} {...props} />
}

export {
  Toggle,
  ToggleIcon,
  toggleStateClassName,
  toggleTextClassName,
  toggleVariants,
}
```

4. Update the import paths to match your project.

**StyleSheet**

1. 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/toggle.tsx`, `components/primitives/use-render.tsx`.

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

```tsx title="stylesheet/components/ui/toggle.tsx"
import type * as React from "react"
import {
  Pressable,
  type StyleProp,
  StyleSheet,
  type TextStyle,
  type ViewStyle,
} from "react-native"
import {
  Toggle as TogglePrimitive,
  type ToggleState,
} from "@/components/primitives/toggle"
import { resolveStyle } from "@/components/primitives/use-render"
import { Icon } from "@/components/ui/icon"
import { TextStyleContext } from "@/components/ui/text"
import {
  fontSize,
  radius,
  type ThemeColors,
  useTheme,
} from "@/components/ui/theme"

type ToggleVariant = "default" | "outline"

type ToggleSize = "default" | "sm" | "lg"

type ToggleVariantProps = {
  variant?: ToggleVariant | null
  size?: ToggleSize | null
}

const sizes = StyleSheet.create({
  default: { height: 36, minWidth: 36, paddingHorizontal: 12 },
  sm: { height: 32, minWidth: 32, paddingHorizontal: 12 },
  lg: { height: 40, minWidth: 40, paddingHorizontal: 16 },
})

const styles = StyleSheet.create({
  root: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "center",
    gap: 4,
    borderRadius: radius.lg,
    backgroundColor: "transparent",
  },
  outline: { borderWidth: 1 },
  scaled: { transform: [{ scale: 0.96 }] },
  icon: { flexShrink: 0 },
})

function toggleStyle(
  { variant, size }: ToggleVariantProps,
  colors: ThemeColors,
  state: ToggleState,
  pressing: boolean
): StyleProp<ViewStyle> {
  return [
    styles.root,
    sizes[size ?? "default"],
    variant === "outline" && [styles.outline, { borderColor: colors.border }],
    state.pressed && { backgroundColor: colors.accent },
    pressing && !state.disabled && [styles.scaled, { opacity: 0.8 }],
    state.disabled && { opacity: 0.5 },
  ]
}

function toggleTextStyle(colors: ThemeColors, state: ToggleState): TextStyle {
  const color = state.pressed ? colors.primary : colors.foreground
  return { ...fontSize.sm, color }
}

function Toggle({
  style,
  variant,
  size,
  ...props
}: React.ComponentProps<typeof TogglePrimitive> & ToggleVariantProps) {
  const { colors } = useTheme()
  return (
    <TogglePrimitive
      {...props}
      render={(renderProps, state) => (
        <TextStyleContext.Provider value={toggleTextStyle(colors, state)}>
          <Pressable
            {...renderProps}
            style={({ pressed }) => [
              toggleStyle({ variant, size }, colors, state, pressed),
              resolveStyle(style, state) as StyleProp<ViewStyle>,
            ]}
          />
        </TextStyleContext.Provider>
      )}
    />
  )
}

function ToggleIcon({
  style,
  size = 16,
  ...props
}: React.ComponentProps<typeof Icon>) {
  return <Icon size={size} style={[styles.icon, style]} {...props} />
}

export type { ToggleSize, ToggleVariant, ToggleVariantProps }
export { Toggle, ToggleIcon, toggleStyle, toggleTextStyle }
```

3. 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/Toggle.swift"
import SwiftUI

public enum ToggleVariant: CaseIterable, Sendable {
  case `default`, outline
}

public enum ToggleSize: CaseIterable, Sendable {
  case `default`, sm, lg

  var height: CGFloat {
    switch self {
    case .default: 36
    case .sm: 32
    case .lg: 40
    }
  }

  var horizontalPadding: CGFloat {
    self == .lg ? 16 : 12
  }
}

public struct VariantToggleStyle: ToggleStyle {
  let variant: ToggleVariant
  let size: ToggleSize

  public init(_ variant: ToggleVariant = .default, size: ToggleSize = .default) {
    self.variant = variant
    self.size = size
  }

  public func makeBody(configuration: Configuration) -> some View {
    HapticButton(style: PressReader { pressed in
      ToggleSurface(isOn: configuration.isOn, pressed: pressed, variant: variant, size: size) {
        configuration.label
      }
    }) {
      configuration.isOn.toggle()
      return true
    }
    .accessibilityAddTraits(configuration.isOn ? .isSelected : [])
  }
}

extension ToggleStyle where Self == VariantToggleStyle {
  public static func variant(_ variant: ToggleVariant = .default, size: ToggleSize = .default) -> VariantToggleStyle {
    VariantToggleStyle(variant, size: size)
  }
}

struct ToggleSurface<Content: View>: View {
  let isOn: Bool
  let pressed: Bool
  let variant: ToggleVariant
  let size: ToggleSize
  var shape = AnyShape(RoundedRectangle(cornerRadius: Radius.lg, style: .continuous))
  var bordered = true
  let content: Content

  @Environment(\.isEnabled) private var isEnabled
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  init(
    isOn: Bool,
    pressed: Bool,
    variant: ToggleVariant,
    size: ToggleSize,
    shape: AnyShape? = nil,
    bordered: Bool = true,
    @ViewBuilder content: () -> Content
  ) {
    self.isOn = isOn
    self.pressed = pressed
    self.variant = variant
    self.size = size
    if let shape { self.shape = shape }
    self.bordered = bordered
    self.content = content()
  }

  var body: some View {
    content
      .labelStyle(ButtonLabelStyle(spacing: 4, iconSize: 16))
      .scaledFont(.sm)
      .lineLimit(1)
      .foregroundStyle(foreground)
      .padding(.horizontal, size.horizontalPadding)
      .frame(minWidth: size.height, minHeight: size.height)
      .background(background, in: shape)
      .overlay {
        if variant == .outline && bordered {
          shape.stroke(EsuissColor.border, lineWidth: 1).padding(0.5)
        }
      }
      .contentShape(shape)
      .opacity(isEnabled ? (pressed ? 0.8 : 1) : 0.5)
      .scaleEffect(pressed && isEnabled && !reduceMotion ? 0.96 : 1)
      .animation(reduceMotion ? nil : .easeOut(duration: 0.15), value: pressed)
  }

  private var foreground: Color {
    isOn ? EsuissColor.primary : EsuissColor.foreground
  }

  private var background: Color {
    isOn ? EsuissColor.accent : .clear
  }
}
```

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

- `swift/Components/ButtonGroup.swift`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

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

- `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/Toggle.kt"
package com.esuiss.ui.components

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
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.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
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.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.esuiss.ui.theme.Colors
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography

enum class ToggleVariant { Default, Outline }

enum class ToggleSize(internal val height: Dp, internal val horizontalPadding: Dp) {
    Default(36.dp, 12.dp),
    Sm(32.dp, 12.dp),
    Lg(40.dp, 16.dp),
}

@Composable
fun Toggle(
    pressed: Boolean,
    onPressedChange: (Boolean) -> Unit,
    modifier: Modifier = Modifier,
    variant: ToggleVariant = ToggleVariant.Default,
    size: ToggleSize = ToggleSize.Default,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource? = null,
    content: @Composable RowScope.() -> Unit,
) {
    val source = interactionSource ?: remember { MutableInteractionSource() }
    val touching by source.collectIsPressedAsState()
    val colors = EsuissTheme.colors
    val haptics = rememberHaptics()
    ToggleSurface(
        modifier = modifier.toggleSemantics(pressed, enabled, source, multiple = true) {
            haptics.toggle(!pressed)
            onPressedChange(!pressed)
        },
        size = size,
        shape = RoundedCornerShape(Radius.lg),
        outlined = variant == ToggleVariant.Outline,
        background = toggleBackground(colors, pressed),
        foreground = toggleForeground(colors, pressed),
        scaled = touching && enabled,
        alpha = toggleAlpha(enabled, touching),
        content = content,
    )
}

@Composable
fun Toggle(
    text: String,
    pressed: Boolean,
    onPressedChange: (Boolean) -> Unit,
    modifier: Modifier = Modifier,
    variant: ToggleVariant = ToggleVariant.Default,
    size: ToggleSize = ToggleSize.Default,
    enabled: Boolean = true,
) {
    Toggle(pressed, onPressedChange, modifier, variant, size, enabled) {
        BasicText(text, style = LocalContentStyle.current, maxLines = 1)
    }
}

internal fun toggleBackground(colors: Colors, on: Boolean): Color =
    if (on) colors.accent else Color.Transparent

internal fun toggleForeground(colors: Colors, on: Boolean): Color =
    if (on) colors.primary else colors.foreground

internal fun toggleAlpha(enabled: Boolean, touching: Boolean): Float = when {
    !enabled -> 0.5f
    touching -> 0.8f
    else -> 1f
}

@Composable
internal fun ToggleSurface(
    modifier: Modifier,
    size: ToggleSize,
    shape: Shape,
    outlined: Boolean,
    background: Color,
    foreground: Color,
    scaled: Boolean,
    alpha: Float = 1f,
    horizontalPadding: Dp = size.horizontalPadding,
    content: @Composable RowScope.() -> Unit,
) {
    val colors = EsuissTheme.colors
    val scale by animateFloatAsState(if (scaled) 0.96f else 1f, tween(150), label = "toggle")
    Row(
        modifier = modifier
            .graphicsLayer {
                scaleX = scale
                scaleY = scale
                this.alpha = alpha
            }
            .height(size.height)
            .defaultMinSize(minWidth = size.height)
            .clip(shape)
            .background(background, shape)
            .then(if (outlined) Modifier.border(1.dp, colors.border, shape) else Modifier)
            .padding(horizontal = horizontalPadding),
        horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        CompositionLocalProvider(
            LocalContentStyle provides Typography.sm.copy(color = foreground),
            LocalContentColor provides foreground,
            LocalIconSize provides 16.dp,
        ) {
            content()
        }
    }
}

internal fun Modifier.toggleSemantics(
    on: Boolean,
    enabled: Boolean,
    source: MutableInteractionSource,
    multiple: Boolean,
    onClick: () -> Unit,
): Modifier = if (multiple) {
    toggleable(
        value = on,
        interactionSource = source,
        indication = null,
        enabled = enabled,
        role = Role.Checkbox,
        onValueChange = { onClick() },
    )
} else {
    selectable(
        selected = on,
        interactionSource = source,
        indication = null,
        enabled = enabled,
        role = Role.RadioButton,
        onClick = onClick,
    )
}
```

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

- `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 { Toggle, ToggleIcon } from "@/components/ui/toggle"
```

```tsx
<View className="flex-row">
  <ToggleDemo accessibilityLabel="Toggle bold">
    <ToggleIcon as={TextBIcon} />
  </ToggleDemo>
</View>
```

Control the state with `pressed` and `onPressedChange(pressed, details)`, or leave it uncontrolled with `defaultPressed`. Put text in a `Text` and icons in `ToggleIcon`.

Icon-only toggles need an `accessibilityLabel`. To group toggles with single or multiple selection, use Toggle Group. `toggleVariants`, `toggleStateClassName` and `toggleTextClassName` are exported for reuse in custom components.

## Examples

### Pressed

An icon toggle that starts in the on state.

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

```tsx
import { TextBIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Toggle, ToggleIcon } from "@/components/ui/toggle"

type ToggleDemoProps = Omit<
  React.ComponentProps<typeof Toggle>,
  "pressed" | "onPressedChange"
> & {
  initial?: boolean
}

function ToggleDemo({ initial = false, children, ...props }: ToggleDemoProps) {
  const [pressed, setPressed] = React.useState(initial)
  return (
    <Toggle pressed={pressed} onPressedChange={setPressed} {...props}>
      {children}
    </Toggle>
  )
}

export function TogglePressedExample() {
  return (
    <View className="flex-row">
      <ToggleDemo initial accessibilityLabel="Toggle bold">
        <ToggleIcon as={TextBIcon} />
      </ToggleDemo>
    </View>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TogglePressedExample: View {
  var body: some View {
    ToggleDemo(initial: true, label: "Toggle bold") { Ph.textB.regular }
  }
}

struct ToggleDemo<Icon: View>: View {
  let variant: ToggleVariant
  let size: ToggleSize
  let label: LocalizedStringKey
  let icon: Icon

  @State private var isOn: Bool

  init(
    initial: Bool = false,
    variant: ToggleVariant = .default,
    size: ToggleSize = .default,
    label: LocalizedStringKey,
    @ViewBuilder icon: () -> Icon
  ) {
    self.variant = variant
    self.size = size
    self.label = label
    self.icon = icon()
    _isOn = State(initialValue: initial)
  }

  var body: some View {
    Toggle(isOn: $isOn) {
      icon.frame(width: 16, height: 16)
    }
    .toggleStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
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.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.TextB
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Toggle
import com.esuiss.ui.components.ToggleSize
import com.esuiss.ui.components.ToggleVariant

@Composable
fun TogglePressedExample() {
    ToggleBold(initial = true)
}

@Composable
fun ToggleDemo(
    label: String,
    initial: Boolean = false,
    variant: ToggleVariant = ToggleVariant.Default,
    size: ToggleSize = ToggleSize.Default,
    enabled: Boolean = true,
    content: @Composable RowScope.() -> Unit,
) {
    var pressed by rememberSaveable { mutableStateOf(initial) }
    Toggle(
        pressed,
        { pressed = it },
        Modifier.semantics { contentDescription = label },
        variant = variant,
        size = size,
        enabled = enabled,
        content = content,
    )
}

@Composable
fun ToggleRow(content: @Composable () -> Unit) {
    Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
        content()
    }
}

@Composable
fun ToggleBold(initial: Boolean = false) {
    ToggleRow {
        ToggleDemo("Toggle bold", initial = initial) {
            Icon(PhosphorIcons.Regular.TextB, contentDescription = null)
        }
    }
}
```

### Variants

The `default` and `outline` variants.

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

```tsx
import { TextItalicIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Toggle, ToggleIcon } from "@/components/ui/toggle"

type ToggleDemoProps = Omit<
  React.ComponentProps<typeof Toggle>,
  "pressed" | "onPressedChange"
> & {
  initial?: boolean
}

function ToggleDemo({ initial = false, children, ...props }: ToggleDemoProps) {
  const [pressed, setPressed] = React.useState(initial)
  return (
    <Toggle pressed={pressed} onPressedChange={setPressed} {...props}>
      {children}
    </Toggle>
  )
}

export function ToggleVariantsExample() {
  return (
    <View className="flex-row gap-2">
      <ToggleDemo accessibilityLabel="Toggle italic">
        <ToggleIcon as={TextItalicIcon} />
      </ToggleDemo>
      <ToggleDemo variant="outline" accessibilityLabel="Toggle italic">
        <ToggleIcon as={TextItalicIcon} />
      </ToggleDemo>
    </View>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ToggleVariantsExample: View {
  var body: some View {
    HStack(spacing: 8) {
      ToggleDemo(label: "Toggle italic") { Ph.textItalic.regular }
      ToggleDemo(variant: .outline, label: "Toggle italic") { Ph.textItalic.regular }
    }
  }
}

struct ToggleDemo<Icon: View>: View {
  let variant: ToggleVariant
  let size: ToggleSize
  let label: LocalizedStringKey
  let icon: Icon

  @State private var isOn: Bool

  init(
    initial: Bool = false,
    variant: ToggleVariant = .default,
    size: ToggleSize = .default,
    label: LocalizedStringKey,
    @ViewBuilder icon: () -> Icon
  ) {
    self.variant = variant
    self.size = size
    self.label = label
    self.icon = icon()
    _isOn = State(initialValue: initial)
  }

  var body: some View {
    Toggle(isOn: $isOn) {
      icon.frame(width: 16, height: 16)
    }
    .toggleStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
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.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.TextItalic
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Toggle
import com.esuiss.ui.components.ToggleSize
import com.esuiss.ui.components.ToggleVariant

@Composable
fun ToggleDemo(
    label: String,
    initial: Boolean = false,
    variant: ToggleVariant = ToggleVariant.Default,
    size: ToggleSize = ToggleSize.Default,
    enabled: Boolean = true,
    content: @Composable RowScope.() -> Unit,
) {
    var pressed by rememberSaveable { mutableStateOf(initial) }
    Toggle(
        pressed,
        { pressed = it },
        Modifier.semantics { contentDescription = label },
        variant = variant,
        size = size,
        enabled = enabled,
        content = content,
    )
}

@Composable
fun ToggleRow(content: @Composable () -> Unit) {
    Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
        content()
    }
}

@Composable
fun ToggleVariants() {
    ToggleRow {
        ToggleDemo("Toggle italic") {
            Icon(PhosphorIcons.Regular.TextItalic, contentDescription = null)
        }
        ToggleDemo("Toggle italic", variant = ToggleVariant.Outline) {
            Icon(PhosphorIcons.Regular.TextItalic, contentDescription = null)
        }
    }
}
```

### Sizes

Outline toggles in the `sm`, default and `lg` sizes.

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

```tsx
import { TextBIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Toggle, ToggleIcon } from "@/components/ui/toggle"

type ToggleDemoProps = Omit<
  React.ComponentProps<typeof Toggle>,
  "pressed" | "onPressedChange"
> & {
  initial?: boolean
}

function ToggleDemo({ initial = false, children, ...props }: ToggleDemoProps) {
  const [pressed, setPressed] = React.useState(initial)
  return (
    <Toggle pressed={pressed} onPressedChange={setPressed} {...props}>
      {children}
    </Toggle>
  )
}

export function ToggleSizesExample() {
  return (
    <View className="flex-row items-center gap-2">
      <ToggleDemo size="sm" variant="outline" accessibilityLabel="Small">
        <ToggleIcon as={TextBIcon} />
      </ToggleDemo>
      <ToggleDemo variant="outline" accessibilityLabel="Default">
        <ToggleIcon as={TextBIcon} />
      </ToggleDemo>
      <ToggleDemo size="lg" variant="outline" accessibilityLabel="Large">
        <ToggleIcon as={TextBIcon} />
      </ToggleDemo>
    </View>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ToggleSizesExample: View {
  var body: some View {
    HStack(spacing: 8) {
      ToggleDemo(variant: .outline, size: .sm, label: "Small") { Ph.textB.regular }
      ToggleDemo(variant: .outline, label: "Default") { Ph.textB.regular }
      ToggleDemo(variant: .outline, size: .lg, label: "Large") { Ph.textB.regular }
    }
  }
}

struct ToggleDemo<Icon: View>: View {
  let variant: ToggleVariant
  let size: ToggleSize
  let label: LocalizedStringKey
  let icon: Icon

  @State private var isOn: Bool

  init(
    initial: Bool = false,
    variant: ToggleVariant = .default,
    size: ToggleSize = .default,
    label: LocalizedStringKey,
    @ViewBuilder icon: () -> Icon
  ) {
    self.variant = variant
    self.size = size
    self.label = label
    self.icon = icon()
    _isOn = State(initialValue: initial)
  }

  var body: some View {
    Toggle(isOn: $isOn) {
      icon.frame(width: 16, height: 16)
    }
    .toggleStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
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.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.TextB
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Toggle
import com.esuiss.ui.components.ToggleSize
import com.esuiss.ui.components.ToggleVariant

@Composable
fun ToggleDemo(
    label: String,
    initial: Boolean = false,
    variant: ToggleVariant = ToggleVariant.Default,
    size: ToggleSize = ToggleSize.Default,
    enabled: Boolean = true,
    content: @Composable RowScope.() -> Unit,
) {
    var pressed by rememberSaveable { mutableStateOf(initial) }
    Toggle(
        pressed,
        { pressed = it },
        Modifier.semantics { contentDescription = label },
        variant = variant,
        size = size,
        enabled = enabled,
        content = content,
    )
}

@Composable
fun ToggleRow(content: @Composable () -> Unit) {
    Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
        content()
    }
}

@Composable
fun ToggleSizes() {
    ToggleRow {
        listOf(ToggleSize.Sm to "Small", ToggleSize.Default to "Default", ToggleSize.Lg to "Large").forEach { (size, label) ->
            key(label) {
                ToggleDemo(label, variant = ToggleVariant.Outline, size = size) {
                    Icon(PhosphorIcons.Regular.TextB, contentDescription = null)
                }
            }
        }
    }
}
```

### With text

An outline toggle with an icon and a text label.

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

```tsx
import { BookmarkSimpleIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Text } from "@/components/ui/text"
import { Toggle, ToggleIcon } from "@/components/ui/toggle"

type ToggleDemoProps = Omit<
  React.ComponentProps<typeof Toggle>,
  "pressed" | "onPressedChange"
> & {
  initial?: boolean
}

function ToggleDemo({ initial = false, children, ...props }: ToggleDemoProps) {
  const [pressed, setPressed] = React.useState(initial)
  return (
    <Toggle pressed={pressed} onPressedChange={setPressed} {...props}>
      {children}
    </Toggle>
  )
}

export function ToggleWithTextExample() {
  return (
    <View className="flex-row">
      <ToggleDemo variant="outline" accessibilityLabel="Bookmark">
        <ToggleIcon as={BookmarkSimpleIcon} />
        <Text>Bookmark</Text>
      </ToggleDemo>
    </View>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct WithTextDemo: View {
  @State private var isOn = false

  var body: some View {
    Toggle(isOn: $isOn) {
      Label {
        Text("Bookmark")
      } icon: {
        Ph.bookmarkSimple.regular
      }
    }
    .toggleStyle(.variant(.outline))
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
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.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.BookmarkSimple
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.Toggle
import com.esuiss.ui.components.ToggleSize
import com.esuiss.ui.components.ToggleVariant

@Composable
fun ToggleDemo(
    label: String,
    initial: Boolean = false,
    variant: ToggleVariant = ToggleVariant.Default,
    size: ToggleSize = ToggleSize.Default,
    enabled: Boolean = true,
    content: @Composable RowScope.() -> Unit,
) {
    var pressed by rememberSaveable { mutableStateOf(initial) }
    Toggle(
        pressed,
        { pressed = it },
        Modifier.semantics { contentDescription = label },
        variant = variant,
        size = size,
        enabled = enabled,
        content = content,
    )
}

@Composable
fun ToggleRow(content: @Composable () -> Unit) {
    Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
        content()
    }
}

@Composable
fun ToggleWithText() {
    ToggleRow {
        ToggleDemo("Bookmark", variant = ToggleVariant.Outline) {
            Icon(PhosphorIcons.Regular.BookmarkSimple, contentDescription = null)
            Text("Bookmark")
        }
    }
}
```

### Disabled

Disabled toggles in the off and on states.

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

```tsx
import { TextBIcon, TextItalicIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Toggle, ToggleIcon } from "@/components/ui/toggle"

type ToggleDemoProps = Omit<
  React.ComponentProps<typeof Toggle>,
  "pressed" | "onPressedChange"
> & {
  initial?: boolean
}

function ToggleDemo({ initial = false, children, ...props }: ToggleDemoProps) {
  const [pressed, setPressed] = React.useState(initial)
  return (
    <Toggle pressed={pressed} onPressedChange={setPressed} {...props}>
      {children}
    </Toggle>
  )
}

export function ToggleDisabledExample() {
  return (
    <View className="flex-row gap-2">
      <ToggleDemo disabled accessibilityLabel="Toggle bold">
        <ToggleIcon as={TextBIcon} />
      </ToggleDemo>
      <ToggleDemo
        disabled
        initial
        variant="outline"
        accessibilityLabel="Toggle italic"
      >
        <ToggleIcon as={TextItalicIcon} />
      </ToggleDemo>
    </View>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct ToggleDisabledExample: View {
  var body: some View {
    HStack(spacing: 8) {
      ToggleDemo(label: "Toggle bold") { Ph.textB.regular }
      ToggleDemo(initial: true, variant: .outline, label: "Toggle italic") { Ph.textItalic.regular }
    }
    .disabled(true)
  }
}

struct ToggleDemo<Icon: View>: View {
  let variant: ToggleVariant
  let size: ToggleSize
  let label: LocalizedStringKey
  let icon: Icon

  @State private var isOn: Bool

  init(
    initial: Bool = false,
    variant: ToggleVariant = .default,
    size: ToggleSize = .default,
    label: LocalizedStringKey,
    @ViewBuilder icon: () -> Icon
  ) {
    self.variant = variant
    self.size = size
    self.label = label
    self.icon = icon()
    _isOn = State(initialValue: initial)
  }

  var body: some View {
    Toggle(isOn: $isOn) {
      icon.frame(width: 16, height: 16)
    }
    .toggleStyle(.variant(variant, size: size))
    .accessibilityLabel(Text(label))
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
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.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.TextB
import com.adamglin.phosphoricons.regular.TextItalic
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Toggle
import com.esuiss.ui.components.ToggleSize
import com.esuiss.ui.components.ToggleVariant

@Composable
fun ToggleDemo(
    label: String,
    initial: Boolean = false,
    variant: ToggleVariant = ToggleVariant.Default,
    size: ToggleSize = ToggleSize.Default,
    enabled: Boolean = true,
    content: @Composable RowScope.() -> Unit,
) {
    var pressed by rememberSaveable { mutableStateOf(initial) }
    Toggle(
        pressed,
        { pressed = it },
        Modifier.semantics { contentDescription = label },
        variant = variant,
        size = size,
        enabled = enabled,
        content = content,
    )
}

@Composable
fun ToggleRow(content: @Composable () -> Unit) {
    Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
        content()
    }
}

@Composable
fun ToggleDisabled() {
    ToggleRow {
        ToggleDemo("Toggle bold", enabled = false) {
            Icon(PhosphorIcons.Regular.TextB, contentDescription = null)
        }
        ToggleDemo("Toggle italic", initial = true, variant = ToggleVariant.Outline, enabled = false) {
            Icon(PhosphorIcons.Regular.TextItalic, contentDescription = null)
        }
    }
}
```

## API Reference

### Toggle

The pressable two-state button.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "outline"` | `"default"` | Visual style: `default` or `outline`. |
| `size` | `"default" \| "sm" \| "lg"` | `"default"` | Height and padding: `default`, `sm` or `lg`. |

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

### ToggleIcon

An icon sized for a toggle.

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

## Accessibility

- Uses the `togglebutton` role and reports `checked` and `disabled` in `accessibilityState`.
- Inside a single-select Toggle Group, it uses the `radio` role instead.
- Icon-only toggles need an `accessibilityLabel`.

## Notes

- Changing the state triggers a selection haptic when a haptics adapter is provided through `HapticsContext`.
- Uses the `bg-accent`, `text-primary` and `border-border` tokens, with `opacity-80` while pressed and `opacity-50` when disabled.
- Light and dark colors follow the system appearance.

