# Collapsible

A section that people can expand and collapse to show or hide its content.

Collapsible 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/collapsible

**React**

**Example**

```tsx
import { CaretUpDownIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"

const repositories = ["acme/ui", "acme/docs", "acme/cli"]

export default function CollapsibleDemo() {
  return (
    <div className="w-80">
      <Collapsible className="flex flex-col gap-2">
        <div className="flex items-center justify-between gap-4 px-3">
          <span className="text-sm font-medium">
            @ademceper starred 3 repositories
          </span>
          <CollapsibleTrigger
            render={
              <Button variant="ghost" size="icon-sm" aria-label="Toggle list" />
            }
          >
            <CaretUpDownIcon />
          </CollapsibleTrigger>
        </div>
        <div className="bg-muted rounded-2xl px-4 py-2 text-sm">
          {repositories[0]}
        </div>
        <CollapsibleContent className="flex flex-col gap-2">
          {repositories.slice(1).map((repository) => (
            <div
              key={repository}
              className="bg-muted rounded-2xl px-4 py-2 text-sm"
            >
              {repository}
            </div>
          ))}
        </CollapsibleContent>
      </Collapsible>
    </div>
  )
}
```

`Collapsible` wraps the collapsible primitive of the chosen base. The `Collapsible` root holds the open state, a `CollapsibleTrigger` toggles it, and `CollapsibleContent` holds the content that is shown or hidden. The parts are unstyled, so you control layout and appearance with `className`, or render the trigger as another component such as `Button`.

Render the trigger as another component with the `render` prop.

Use a collapsible for a single disclosure region. For a set of related, stacked sections, use an accordion instead.

## Installation

### CLI

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

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

### Manual

1. Install the dependencies:

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

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

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

import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"

function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
  return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}

function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
  return (
    <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
  )
}

function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
  return (
    <CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
  )
}

export { Collapsible, CollapsibleContent, CollapsibleTrigger }
```

3. Update the import paths to match your project.

## Usage

```tsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"
```

```tsx
<div className="w-80">
  <Collapsible className="flex flex-col gap-2">
    <div className="flex items-center justify-between gap-4 px-3">
      <span className="text-sm font-medium">
        @ademceper starred 3 repositories
      </span>
      <CollapsibleTrigger
        render={
          <Button variant="ghost" size="icon-sm" aria-label="Toggle list" />
        }
      >
        <CaretUpDownIcon />
      </CollapsibleTrigger>
    </div>
    <div className="bg-muted rounded-2xl px-4 py-2 text-sm">
      {repositories[0]}
    </div>
    <CollapsibleContent className="flex flex-col gap-2">
      {repositories.slice(1).map((repository) => (
        <div
          key={repository}
          className="bg-muted rounded-2xl px-4 py-2 text-sm"
        >
          {repository}
        </div>
      ))}
    </CollapsibleContent>
  </Collapsible>
</div>
```

The root is uncontrolled by default; set `defaultOpen` to start expanded. To control it, pass `open` and `onOpenChange`. Set `disabled` on the root to prevent toggling. While collapsed, the content is removed from the page.

## Examples

### By default

A collapsible that starts expanded using `defaultOpen`.

**Example**

```tsx
import { CaretUpDownIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"

const repositories = ["acme/ui", "acme/docs", "acme/cli"]

export default function CollapsibleOpenByDefault() {
  return (
    <div className="w-80">
      <Collapsible defaultOpen className="flex flex-col gap-2">
        <div className="flex items-center justify-between gap-4 px-3">
          <span className="text-sm font-medium">
            @ademceper starred 3 repositories
          </span>
          <CollapsibleTrigger
            render={
              <Button variant="ghost" size="icon-sm" aria-label="Toggle list" />
            }
          >
            <CaretUpDownIcon />
          </CollapsibleTrigger>
        </div>
        <div className="bg-muted rounded-2xl px-4 py-2 text-sm">
          {repositories[0]}
        </div>
        <CollapsibleContent className="flex flex-col gap-2">
          {repositories.slice(1).map((repository) => (
            <div
              key={repository}
              className="bg-muted rounded-2xl px-4 py-2 text-sm"
            >
              {repository}
            </div>
          ))}
        </CollapsibleContent>
      </Collapsible>
    </div>
  )
}
```

### Disabled

A disabled collapsible whose trigger does not toggle the content.

**Example**

```tsx
import { CaretUpDownIcon } from "@phosphor-icons/react"
import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"

const repositories = ["acme/ui", "acme/docs", "acme/cli"]

export default function CollapsibleDisabled() {
  return (
    <div className="w-80">
      <Collapsible disabled className="flex flex-col gap-2">
        <div className="flex items-center justify-between gap-4 px-3">
          <span className="text-sm font-medium">
            @ademceper starred 3 repositories
          </span>
          <CollapsibleTrigger
            render={
              <Button variant="ghost" size="icon-sm" aria-label="Toggle list" />
            }
          >
            <CaretUpDownIcon />
          </CollapsibleTrigger>
        </div>
        <div className="bg-muted rounded-2xl px-4 py-2 text-sm">
          {repositories[0]}
        </div>
        <CollapsibleContent className="flex flex-col gap-2">
          {repositories.slice(1).map((repository) => (
            <div
              key={repository}
              className="bg-muted rounded-2xl px-4 py-2 text-sm"
            >
              {repository}
            </div>
          ))}
        </CollapsibleContent>
      </Collapsible>
    </div>
  )
}
```

## API Reference

- `Collapsible`
- `CollapsibleTrigger`
- `CollapsibleContent`

### Collapsible

The root that holds the open state and groups the trigger and content.

Also accepts the props of the primitive part it wraps.

### CollapsibleTrigger

The button that toggles the content open and closed.

Also accepts the props of the primitive part it wraps.

### CollapsibleContent

The panel that is shown when the collapsible is open.

Also accepts the props of the primitive part it wraps.

## Accessibility

- The trigger renders as a button and sets `aria-expanded` to reflect the open state.
- The trigger toggles on click, and with Enter or Space when focused.
- Give an icon-only trigger an accessible name with `aria-label`.

## Notes

- Styling hooks: `data-slot="collapsible"`, `data-slot="collapsible-trigger"` and `data-slot="collapsible-content"`.
- The primitive sets state attributes on the parts, which you can use for styling and animation.
- No styles are applied by default.

**React Native, SwiftUI, Jetpack Compose**

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

```tsx
import { CaretUpDownIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

const repositories = ["esuiss/ui", "esuiss/docs", "esuiss/cli"]

function CollapsibleDemo({
  initialOpen = false,
  disabled = false,
}: {
  initialOpen?: boolean
  disabled?: boolean
}) {
  const [open, setOpen] = React.useState(initialOpen)
  return (
    <Collapsible
      open={open}
      onOpenChange={setOpen}
      disabled={disabled}
      className="w-full gap-2"
    >
      <View className="flex-row items-center justify-between gap-4 px-3">
        <Text className="text-sm font-medium">
          @ademceper starred 3 repositories
        </Text>
        <CollapsibleTrigger
          render={
            <Button variant="ghost" size="icon-sm" disabled={disabled}>
              <Icon as={CaretUpDownIcon} className="size-4" />
            </Button>
          }
        />
      </View>
      <View className="rounded-2xl bg-muted px-4 py-2">
        <Text className="text-sm">{repositories[0]}</Text>
      </View>
      <CollapsibleContent className="gap-2">
        {repositories.slice(1).map((repository) => (
          <View key={repository} className="rounded-2xl bg-muted px-4 py-2">
            <Text className="text-sm">{repository}</Text>
          </View>
        ))}
      </CollapsibleContent>
    </Collapsible>
  )
}

export function CollapsibleOpenByDefaultExample() {
  return (
    <CollapsibleDemo initialOpen />
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct CollapsibleOpenByDefaultExample: View {
  var body: some View {
    CollapsibleDemo(initialOpen: true)
  }
}

let collapsibleRepositories = ["esuiss/ui", "esuiss/docs", "esuiss/cli"]

struct CollapsibleDemo: View {
  var disabled = false

  @State private var open: Bool

  init(initialOpen: Bool = false, disabled: Bool = false) {
    self.disabled = disabled
    _open = State(initialValue: initialOpen)
  }

  var body: some View {
    Collapsible(isExpanded: $open, spacing: 8) {
      HStack(spacing: 16) {
        Text("@ademceper starred 3 repositories")
          .textStyle(.sm, weight: .medium)
          .foregroundStyle(EsuissColor.foreground)
          .frame(maxWidth: .infinity, alignment: .leading)
        CollapsibleTrigger {
          Ph.caretUpDown.regular
            .frame(width: 16, height: 16)
        }
        .buttonStyle(.variant(.ghost, size: .iconSm))
        .disabled(disabled)
      }
      .padding(.horizontal, 12)
      CollapsibleRepository(name: collapsibleRepositories[0])
      CollapsibleContent {
        VStack(spacing: 8) {
          ForEach(collapsibleRepositories.dropFirst(), id: \.self) { repository in
            CollapsibleRepository(name: repository)
          }
        }
      }
    }
    .frame(maxWidth: .infinity)
  }
}

struct CollapsibleRepository: View {
  let name: String

  var body: some View {
    Text(verbatim: name)
      .textStyle(.sm)
      .foregroundStyle(EsuissColor.foreground)
      .padding(.horizontal, 16)
      .padding(.vertical, 8)
      .frame(maxWidth: .infinity, alignment: .leading)
      .background(EsuissColor.muted, in: RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
  }
}
```

```kotlin
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CaretUpDown
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Collapsible
import com.esuiss.ui.components.CollapsibleContent
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography

@Composable
fun CollapsibleOpenByDefaultExample() {
    CollapsibleDemo(initialOpen = true)
}

val CollapsibleRepositories = listOf("esuiss/ui", "esuiss/docs", "esuiss/cli")

@Composable
fun CollapsibleRepository(name: String) {
    Text(
        name,
        modifier = Modifier
            .fillMaxWidth()
            .background(EsuissTheme.colors.muted, RoundedCornerShape(Radius.xl2))
            .padding(horizontal = 16.dp, vertical = 8.dp),
        style = Typography.sm,
    )
}

@Composable
fun CollapsibleDemo(initialOpen: Boolean = false, disabled: Boolean = false) {
    var open by rememberSaveable { mutableStateOf(initialOpen) }
    Collapsible(
        expanded = open,
        onExpandedChange = { open = it },
        modifier = Modifier.fillMaxWidth(),
        enabled = !disabled,
    ) {
        Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
            Row(
                modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
                horizontalArrangement = Arrangement.spacedBy(16.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Text(
                    "@ademceper starred 3 repositories",
                    modifier = Modifier.weight(1f),
                    style = Typography.sm.copy(fontWeight = FontWeight.Medium),
                )
                Button(
                    onClick = { open = !open },
                    variant = ButtonVariant.Ghost,
                    size = ButtonSize.IconSm,
                    enabled = !disabled,
                ) {
                    Icon(PhosphorIcons.Regular.CaretUpDown, contentDescription = "Toggle", size = 16.dp)
                }
            }
            CollapsibleRepository(CollapsibleRepositories[0])
            CollapsibleContent {
                Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
                    CollapsibleRepositories.drop(1).forEach { CollapsibleRepository(it) }
                }
            }
        }
    }
}
```

`Collapsible` is an unstyled root with a `CollapsibleTrigger` and a `CollapsibleContent` panel. It handles the open state and accessibility, and leaves the look to you.

Use it for a single show-more section. For a list of sections where opening one can close another, use Accordion.

## Installation

**React Native**

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

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

**SwiftUI**

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

**Jetpack Compose**

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

### Manual

**React Native**

**Uniwind**

1. Add the pieces it builds on: the primitives `components/primitives/collapsible.tsx`.

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

```tsx title="uniwind/components/ui/collapsible.tsx"
import * as CollapsiblePrimitive from "@/components/primitives/collapsible"

const Collapsible = CollapsiblePrimitive.Root

const CollapsibleTrigger = CollapsiblePrimitive.Trigger

const CollapsibleContent = CollapsiblePrimitive.Panel

export { Collapsible, CollapsibleContent, CollapsibleTrigger }
```

3. Update the import paths to match your project.

**NativeWind**

1. Add the pieces it builds on: the primitives `components/primitives/collapsible.tsx`.

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

```tsx title="nativewind/components/ui/collapsible.tsx"
import * as CollapsiblePrimitive from "@/components/primitives/collapsible"

const Collapsible = CollapsiblePrimitive.Root

const CollapsibleTrigger = CollapsiblePrimitive.Trigger

const CollapsibleContent = CollapsiblePrimitive.Panel

export { Collapsible, CollapsibleContent, CollapsibleTrigger }
```

3. Update the import paths to match your project.

**StyleSheet**

1. Add the pieces it builds on: the primitives `components/primitives/collapsible.tsx`.

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

```tsx title="stylesheet/components/ui/collapsible.tsx"
import * as CollapsiblePrimitive from "@/components/primitives/collapsible"

const Collapsible = CollapsiblePrimitive.Root

const CollapsibleTrigger = CollapsiblePrimitive.Trigger

const CollapsibleContent = CollapsiblePrimitive.Panel

export { Collapsible, CollapsibleContent, CollapsibleTrigger }
```

3. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. 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)

3. Copy the source:

```swift title="swift/Components/Collapsible.swift"
import SwiftUI

public struct Collapsible<Content: View>: View {
  let external: Binding<Bool>?
  let spacing: CGFloat
  let content: Content

  @State private var local = false
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init(
    isExpanded: Binding<Bool>,
    spacing: CGFloat = 0,
    @ViewBuilder content: () -> Content
  ) {
    self.external = isExpanded
    self.spacing = spacing
    self.content = content()
  }

  public init(spacing: CGFloat = 0, @ViewBuilder content: () -> Content) {
    self.external = nil
    self.spacing = spacing
    self.content = content()
  }

  public var body: some View {
    let binding = external ?? $local
    let animation: Animation = reduceMotion ? .linear(duration: 0.15) : .easeInOut(duration: 0.2)
    VStack(alignment: .leading, spacing: spacing) {
      content
    }
    .environment(
      \.collapsible,
      CollapsibleContext(
        isExpanded: binding.wrappedValue,
        toggle: { withAnimation(animation) { binding.wrappedValue.toggle() } }
      )
    )
  }
}

struct CollapsibleContext: Sendable {
  var isExpanded = false
  var toggle: @MainActor @Sendable () -> Void = {}
}

extension EnvironmentValues {
  @Entry var collapsible = CollapsibleContext()
}

public struct CollapsibleTrigger<Label: View>: View {
  let label: Label

  @Environment(\.collapsible) private var collapsible

  public init(@ViewBuilder label: () -> Label) {
    self.label = label()
  }

  public var body: some View {
    Button {
      collapsible.toggle()
    } label: {
      label
    }
    .accessibilityValue(Text(collapsible.isExpanded ? "Expanded" : "Collapsed"))
  }
}

extension CollapsibleTrigger where Label == Text {
  public init(_ title: LocalizedStringKey) {
    self.init { Text(title) }
  }
}

public struct CollapsibleContent<Content: View>: View {
  let content: Content

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

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

  public var body: some View {
    if collapsible.isExpanded {
      content
        .transition(reduceMotion ? .opacity : .opacity.combined(with: .offset(y: -8)))
    }
  }
}
```

4. 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")
```

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

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.collapse
import androidx.compose.ui.semantics.expand
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription

@Immutable
private data class CollapsibleState(
    val expanded: Boolean,
    val enabled: Boolean,
    val onExpandedChange: (Boolean) -> Unit,
)

private val LocalCollapsible = staticCompositionLocalOf<CollapsibleState?> { null }

@Composable
fun Collapsible(
    expanded: Boolean,
    onExpandedChange: (Boolean) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    content: @Composable ColumnScope.() -> Unit,
) {
    CompositionLocalProvider(LocalCollapsible provides CollapsibleState(expanded, enabled, onExpandedChange)) {
        Column(modifier = modifier, content = content)
    }
}

@Composable
fun CollapsibleTrigger(
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit,
) {
    val state = LocalCollapsible.current ?: error("CollapsibleTrigger must be used within a Collapsible.")
    Row(
        modifier = modifier
            .semantics {
                stateDescription = if (state.expanded) "Expanded" else "Collapsed"
                if (state.enabled) {
                    if (state.expanded) {
                        collapse { state.onExpandedChange(false); true }
                    } else {
                        expand { state.onExpandedChange(true); true }
                    }
                }
            }
            .clickable(
                interactionSource = remember { MutableInteractionSource() },
                indication = null,
                enabled = state.enabled,
                role = Role.Button,
                onClick = { state.onExpandedChange(!state.expanded) },
            ),
        verticalAlignment = Alignment.CenterVertically,
        content = content,
    )
}

@Composable
fun CollapsibleContent(
    modifier: Modifier = Modifier,
    content: @Composable ColumnScope.() -> Unit,
) {
    val state = LocalCollapsible.current ?: error("CollapsibleContent must be used within a Collapsible.")
    AnimatedVisibility(
        visible = state.expanded,
        modifier = modifier,
        enter = expandVertically(tween(250), expandFrom = Alignment.Top) + fadeIn(tween(250)),
        exit = shrinkVertically(tween(200), shrinkTowards = Alignment.Top) + fadeOut(tween(200)),
    ) {
        Column(content = content)
    }
}
```

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

- `kotlin/components/ButtonGroup.kt`: see [Button Group](https://ui.suiss.app/docs/components/button-group.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/Icon.kt`: see [Icon](https://ui.suiss.app/docs/components/icon.md)

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

## Usage

```tsx
<CollapsibleDemo initialOpen />
```

Put any content in `CollapsibleTrigger`, for example a `Text` and a caret `Icon`, and use `render` to render it as a `Button`. Use `open` with `onOpenChange` for controlled state, or `defaultOpen` for uncontrolled.

Set `keepMounted` on `CollapsibleContent` to keep the content mounted while closed.

## Examples

### Default

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

```tsx
import { CaretUpDownIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

const repositories = ["esuiss/ui", "esuiss/docs", "esuiss/cli"]

function CollapsibleDemo({
  initialOpen = false,
  disabled = false,
}: {
  initialOpen?: boolean
  disabled?: boolean
}) {
  const [open, setOpen] = React.useState(initialOpen)
  return (
    <Collapsible
      open={open}
      onOpenChange={setOpen}
      disabled={disabled}
      className="w-full gap-2"
    >
      <View className="flex-row items-center justify-between gap-4 px-3">
        <Text className="text-sm font-medium">
          @ademceper starred 3 repositories
        </Text>
        <CollapsibleTrigger
          render={
            <Button variant="ghost" size="icon-sm" disabled={disabled}>
              <Icon as={CaretUpDownIcon} className="size-4" />
            </Button>
          }
        />
      </View>
      <View className="rounded-2xl bg-muted px-4 py-2">
        <Text className="text-sm">{repositories[0]}</Text>
      </View>
      <CollapsibleContent className="gap-2">
        {repositories.slice(1).map((repository) => (
          <View key={repository} className="rounded-2xl bg-muted px-4 py-2">
            <Text className="text-sm">{repository}</Text>
          </View>
        ))}
      </CollapsibleContent>
    </Collapsible>
  )
}

export function CollapsibleExample() {
  return (
    <CollapsibleDemo />
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

let collapsibleRepositories = ["esuiss/ui", "esuiss/docs", "esuiss/cli"]

struct CollapsibleDemo: View {
  var disabled = false

  @State private var open: Bool

  init(initialOpen: Bool = false, disabled: Bool = false) {
    self.disabled = disabled
    _open = State(initialValue: initialOpen)
  }

  var body: some View {
    Collapsible(isExpanded: $open, spacing: 8) {
      HStack(spacing: 16) {
        Text("@ademceper starred 3 repositories")
          .textStyle(.sm, weight: .medium)
          .foregroundStyle(EsuissColor.foreground)
          .frame(maxWidth: .infinity, alignment: .leading)
        CollapsibleTrigger {
          Ph.caretUpDown.regular
            .frame(width: 16, height: 16)
        }
        .buttonStyle(.variant(.ghost, size: .iconSm))
        .disabled(disabled)
      }
      .padding(.horizontal, 12)
      CollapsibleRepository(name: collapsibleRepositories[0])
      CollapsibleContent {
        VStack(spacing: 8) {
          ForEach(collapsibleRepositories.dropFirst(), id: \.self) { repository in
            CollapsibleRepository(name: repository)
          }
        }
      }
    }
    .frame(maxWidth: .infinity)
  }
}

struct CollapsibleRepository: View {
  let name: String

  var body: some View {
    Text(verbatim: name)
      .textStyle(.sm)
      .foregroundStyle(EsuissColor.foreground)
      .padding(.horizontal, 16)
      .padding(.vertical, 8)
      .frame(maxWidth: .infinity, alignment: .leading)
      .background(EsuissColor.muted, in: RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
  }
}
```

```kotlin
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CaretUpDown
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Collapsible
import com.esuiss.ui.components.CollapsibleContent
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography

val CollapsibleRepositories = listOf("esuiss/ui", "esuiss/docs", "esuiss/cli")

@Composable
fun CollapsibleRepository(name: String) {
    Text(
        name,
        modifier = Modifier
            .fillMaxWidth()
            .background(EsuissTheme.colors.muted, RoundedCornerShape(Radius.xl2))
            .padding(horizontal = 16.dp, vertical = 8.dp),
        style = Typography.sm,
    )
}

@Composable
fun CollapsibleDemo(initialOpen: Boolean = false, disabled: Boolean = false) {
    var open by rememberSaveable { mutableStateOf(initialOpen) }
    Collapsible(
        expanded = open,
        onExpandedChange = { open = it },
        modifier = Modifier.fillMaxWidth(),
        enabled = !disabled,
    ) {
        Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
            Row(
                modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
                horizontalArrangement = Arrangement.spacedBy(16.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Text(
                    "@ademceper starred 3 repositories",
                    modifier = Modifier.weight(1f),
                    style = Typography.sm.copy(fontWeight = FontWeight.Medium),
                )
                Button(
                    onClick = { open = !open },
                    variant = ButtonVariant.Ghost,
                    size = ButtonSize.IconSm,
                    enabled = !disabled,
                ) {
                    Icon(PhosphorIcons.Regular.CaretUpDown, contentDescription = "Toggle", size = 16.dp)
                }
            }
            CollapsibleRepository(CollapsibleRepositories[0])
            CollapsibleContent {
                Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
                    CollapsibleRepositories.drop(1).forEach { CollapsibleRepository(it) }
                }
            }
        }
    }
}
```

### Disabled

A disabled collapsible whose trigger cannot toggle it.

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

```tsx
import { CaretUpDownIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"

const repositories = ["esuiss/ui", "esuiss/docs", "esuiss/cli"]

function CollapsibleDemo({
  initialOpen = false,
  disabled = false,
}: {
  initialOpen?: boolean
  disabled?: boolean
}) {
  const [open, setOpen] = React.useState(initialOpen)
  return (
    <Collapsible
      open={open}
      onOpenChange={setOpen}
      disabled={disabled}
      className="w-full gap-2"
    >
      <View className="flex-row items-center justify-between gap-4 px-3">
        <Text className="text-sm font-medium">
          @ademceper starred 3 repositories
        </Text>
        <CollapsibleTrigger
          render={
            <Button variant="ghost" size="icon-sm" disabled={disabled}>
              <Icon as={CaretUpDownIcon} className="size-4" />
            </Button>
          }
        />
      </View>
      <View className="rounded-2xl bg-muted px-4 py-2">
        <Text className="text-sm">{repositories[0]}</Text>
      </View>
      <CollapsibleContent className="gap-2">
        {repositories.slice(1).map((repository) => (
          <View key={repository} className="rounded-2xl bg-muted px-4 py-2">
            <Text className="text-sm">{repository}</Text>
          </View>
        ))}
      </CollapsibleContent>
    </Collapsible>
  )
}

export function CollapsibleDisabledExample() {
  return (
    <CollapsibleDemo disabled />
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct CollapsibleDisabledExample: View {
  var body: some View {
    CollapsibleDemo(disabled: true)
  }
}

let collapsibleRepositories = ["esuiss/ui", "esuiss/docs", "esuiss/cli"]

struct CollapsibleDemo: View {
  var disabled = false

  @State private var open: Bool

  init(initialOpen: Bool = false, disabled: Bool = false) {
    self.disabled = disabled
    _open = State(initialValue: initialOpen)
  }

  var body: some View {
    Collapsible(isExpanded: $open, spacing: 8) {
      HStack(spacing: 16) {
        Text("@ademceper starred 3 repositories")
          .textStyle(.sm, weight: .medium)
          .foregroundStyle(EsuissColor.foreground)
          .frame(maxWidth: .infinity, alignment: .leading)
        CollapsibleTrigger {
          Ph.caretUpDown.regular
            .frame(width: 16, height: 16)
        }
        .buttonStyle(.variant(.ghost, size: .iconSm))
        .disabled(disabled)
      }
      .padding(.horizontal, 12)
      CollapsibleRepository(name: collapsibleRepositories[0])
      CollapsibleContent {
        VStack(spacing: 8) {
          ForEach(collapsibleRepositories.dropFirst(), id: \.self) { repository in
            CollapsibleRepository(name: repository)
          }
        }
      }
    }
    .frame(maxWidth: .infinity)
  }
}

struct CollapsibleRepository: View {
  let name: String

  var body: some View {
    Text(verbatim: name)
      .textStyle(.sm)
      .foregroundStyle(EsuissColor.foreground)
      .padding(.horizontal, 16)
      .padding(.vertical, 8)
      .frame(maxWidth: .infinity, alignment: .leading)
      .background(EsuissColor.muted, in: RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
  }
}
```

```kotlin
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.CaretUpDown
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Collapsible
import com.esuiss.ui.components.CollapsibleContent
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography

@Composable
fun CollapsibleDisabledExample() {
    CollapsibleDemo(disabled = true)
}

val CollapsibleRepositories = listOf("esuiss/ui", "esuiss/docs", "esuiss/cli")

@Composable
fun CollapsibleRepository(name: String) {
    Text(
        name,
        modifier = Modifier
            .fillMaxWidth()
            .background(EsuissTheme.colors.muted, RoundedCornerShape(Radius.xl2))
            .padding(horizontal = 16.dp, vertical = 8.dp),
        style = Typography.sm,
    )
}

@Composable
fun CollapsibleDemo(initialOpen: Boolean = false, disabled: Boolean = false) {
    var open by rememberSaveable { mutableStateOf(initialOpen) }
    Collapsible(
        expanded = open,
        onExpandedChange = { open = it },
        modifier = Modifier.fillMaxWidth(),
        enabled = !disabled,
    ) {
        Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
            Row(
                modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
                horizontalArrangement = Arrangement.spacedBy(16.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Text(
                    "@ademceper starred 3 repositories",
                    modifier = Modifier.weight(1f),
                    style = Typography.sm.copy(fontWeight = FontWeight.Medium),
                )
                Button(
                    onClick = { open = !open },
                    variant = ButtonVariant.Ghost,
                    size = ButtonSize.IconSm,
                    enabled = !disabled,
                ) {
                    Icon(PhosphorIcons.Regular.CaretUpDown, contentDescription = "Toggle", size = 16.dp)
                }
            }
            CollapsibleRepository(CollapsibleRepositories[0])
            CollapsibleContent {
                Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
                    CollapsibleRepositories.drop(1).forEach { CollapsibleRepository(it) }
                }
            }
        }
    }
}
```

## Accessibility

- The trigger has the `button` role with `expanded` and `disabled` state.
- Closed content is hidden from VoiceOver and TalkBack.

