suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Components82
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Wheel Picker
  • Widget

suiss/ui · Component

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.

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.

Set asChild on CollapsibleTrigger to render another component as the trigger.

Any Button with slot="trigger" inside Collapsible toggles it.

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

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/collapsible

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add collapsible

Manual

  1. Install the dependencies:
$ pnpm add @base-ui/react
  1. Copy the source into components/ui/collapsible.tsx:
  1. Update the import paths to match your project.

Usage

import { Collapsible, CollapsibleTrigger, CollapsibleContent } 
<



























<

























<




























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.

Disabled

A disabled collapsible whose trigger does not toggle the content.

API Reference

Anatomy3 parts
<Collapsible>
<CollapsibleTrigger />
<CollapsibleContent />
</Collapsible>
01

Collapsible

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

Also accepts the props of the primitive part it wraps.

02

CollapsibleTrigger

The button that toggles the content open and closed.

Also accepts the props of the primitive part it wraps.

03

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.

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

$ pnpm dlx shadcn@latest add @esuiss-rn/collapsible

See Installation for the components.json setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add collapsible
$ pnpm dlx @esuiss/ui@latest add collapsible --platform swift
NextComboboxPreviousChip

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • By default
    • Disabled
  4. 04API Reference
    • Collapsible
    • CollapsibleTrigger
    • CollapsibleContent
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • Default
    • Disabled
  10. 10Accessibility
from
"@/components/ui/collapsible"
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>
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 asChild>
<Button variant="ghost" size="icon-sm" aria-label="Toggle list">
<CaretUpDownIcon />
</Button>
</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>
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>
<Button
slot="trigger"
variant="ghost"
size="icon-sm"
aria-label="Toggle list"
>
<CaretUpDownIcon />
</Button>
</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>
$ pnpm dlx @esuiss/ui@latest add collapsible --platform kotlin

Manual

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

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

  1. Update the import paths to match your project.
  1. Add the pieces it builds on: the primitives components/primitives/collapsible.tsx.

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

  1. Update the import paths to match your project.
  1. Add the pieces it builds on: the primitives components/primitives/collapsible.tsx.

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

  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Copy the theme files every view uses:

swift/Theme/Theme.swift
import SwiftUI
import UIKit

struct RGBA: Sendable {
  let red: CGFloat
  let green: CGFloat
  let blue: CGFloat
  let alpha: CGFloat

  init(_ hex: UInt32, _ alpha: CGFloat = 1) {
    red = CGFloat((hex >> 16) & 0xFF) / 255
    green = CGFloat((hex >> 8) & 0xFF) / 255
    blue = CGFloat(hex & 0xFF) / 255
    self.alpha = alpha
  }
swift/Theme/Tokens.swift
import SwiftUI

public enum EsuissColor {
  public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
  public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF))
  public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF))
  public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
  public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
  public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
  public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
  1. Copy the source:
  1. Add the files to your app target.
  1. Set minSdk to 24 or higher and add the dependencies:
build.gradle.kts
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
  1. Copy the theme files every view uses:
kotlin/theme/Theme.kt
package com.esuiss.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
kotlin/theme/Tokens.kt
package com.esuiss.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Immutable
data class Colors(
    val background: Color,
    val foreground: Color,
    val card: Color,
    val cardForeground: Color,
    val popover: Color,
    val popoverForeground: Color,
    val primary: Color,
    val primaryForeground: Color,
  1. Copy the source and every file it uses:
kotlin/components/Button.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
kotlin/components/ButtonGroup.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
kotlin/components/Content.kt
package com.esuiss.ui.components

import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

val LocalContentStyle = compositionLocalOf { TextStyle.Default }

val LocalContentColor = compositionLocalOf { Color.Unspecified }

val LocalIconSize = compositionLocalOf { 20.dp }
kotlin/components/Icon.kt
package com.esuiss.ui.components

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Icon(
    imageVector: ImageVector,
  1. Update the com.esuiss.ui package names to match your project.

Usage

<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

iOS
Collapsible on iOS in light modeCollapsible on iOS in dark mode
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 />
  )
}
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))
  }
}
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.

iOS
Collapsible (disabled) on iOS in light modeCollapsible (disabled) on iOS in dark mode
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 />
  )
}
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))
  }
}
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.
iOS
Collapsible (open by default) on iOS in light modeCollapsible (open by default) on iOS in dark mode
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 />
  )
}
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))
  }
}
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) }
                }
            }
        }
    }
}
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>
  )
}
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 (
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>
  )
}
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>
  )
}
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 }
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 }
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
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
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 }