---
name: collapsible
title: Collapsible
url: https://ui.frontic.com/components/ui/collapsible.md
tier: ui
install: npx @frontic/ui add collapsible
files:
  - components/ui/collapsible/Collapsible.vue
  - components/ui/collapsible/CollapsibleContent.vue
  - components/ui/collapsible/CollapsibleTrigger.vue
  - components/ui/collapsible/index.ts
dependencies:
  - reka-ui
---

# Collapsible

Content that opens and closes with a height animation under an unstyled trigger, which takes any element through as-child.

## Install

```sh
npx @frontic/ui add collapsible
```

Used by: [Filter](https://ui.frontic.com/components/ui/filter.md), [Radio Accordion](https://ui.frontic.com/components/ui/radio-accordion.md), [Category Filter](https://ui.frontic.com/components/templates/category-filter.md), [Checkout Page](https://ui.frontic.com/components/templates/checkout-page.md)

npm packages: `reka-ui`

## Examples

```vue
<script setup lang="ts">
import { ChevronDownIcon } from '@lucide/vue'

import { Image } from '@/components/ui/image'
import { Price } from '@/components/ui/price'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'

const image = (name: string) => `https://static.frontic.com/cdn-cgi/image/width=640,format=auto/ui/products/${name}.jpg`
const items = [
  { name: 'Blizzard Defender', detail: 'Aubergine · M', price: { amount: 12600, currency: 'EUR' }, image: image('blizzard-defender-aubergine') },
  { name: 'Polar Light Ski Jacket', detail: 'Dusty pink · S', price: { amount: 7900, currency: 'EUR' }, image: image('polar-light-ski-jacket') },
]
const total = { amount: items.reduce((sum, item) => sum + item.price.amount, 0), currency: 'EUR' }
</script>

<template>
  <Collapsible v-slot="{ open }" class="flex w-sm max-w-full flex-col border-y border-border">
    <CollapsibleTrigger class="flex w-full items-center gap-2 py-4 text-start text-sm">
      <span class="flex-1">{{ open ? 'Hide' : 'Show' }} order summary</span>
      <ChevronDownIcon :class="['size-4 transition-transform', open && 'rotate-180']" />
      <Price :price="total" align="end" />
    </CollapsibleTrigger>
    <CollapsibleContent>
      <ul class="flex flex-col gap-3 pb-4">
        <li v-for="item in items" :key="item.name" class="flex items-center gap-3 text-sm">
          <Image :src="item.image" :alt="item.name" class="size-12 shrink-0 object-cover" />
          <span class="flex flex-1 flex-col">
            <span>{{ item.name }}</span>
            <span class="text-muted-foreground">{{ item.detail }}</span>
          </span>
          <Price :price="item.price" align="end" />
        </li>
      </ul>
    </CollapsibleContent>
  </Collapsible>
</template>
```

The example also uses `image` and `price`, which `add collapsible` leaves out: `npx @frontic/ui add image price`.

## Anatomy

```vue
<Collapsible>
  <CollapsibleTrigger />
  <CollapsibleContent />
</Collapsible>
```

## API reference

### Collapsible

Groups a trigger with content that can be shown or hidden.

`data-slot="collapsible"` · `Collapsible.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultOpen` | `boolean` | `false` | The open state of the collapsible when it is initially rendered.<br><br>Use when you do not need to control its open state. |
| `open` | `boolean` |  | The controlled open state of the collapsible. Can be binded with `v-model`. |
| `disabled` | `boolean` | `false` | When `true`, prevents the user from interacting with the collapsible. |
| `unmountOnHide` | `boolean` | `true` | When `true`, the element will be unmounted on closed state. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` |  | The element or component this component should render as. Can be overwritten by `asChild`. |

| Event | Payload | Description |
| --- | --- | --- |
| `update:open` | `value: boolean` | Event handler called when the open state of the collapsible changes. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ open: boolean; }` | Holds the trigger and the content; gets whether the content is shown. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-disabled` |  | Present while the part is disabled. |
| `data-state` | `'open' \| 'closed'` | The state the part is in, one value per state. |

### CollapsibleContent

Holds what its collapsible shows while open.

`data-slot="collapsible-content"` · `CollapsibleContent.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `forceMount` | `boolean` | `false` | Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` |  | The element or component this component should render as. Can be overwritten by `asChild`. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-disabled` |  | Present while the part is disabled. |
| `data-state` | `'open' \| 'closed'` | The state the part is in, one value per state. |

| CSS variable | Description |
| --- | --- |
| `--reka-collapsible-content-height` | The content's height while open, for animating it open and closed. |
| `--reka-collapsible-content-width` | The content's width while open, for animating it open and closed. |

### CollapsibleTrigger

Opens and closes the content of its collapsible. Renders a `<button>`.

`data-slot="collapsible-trigger"` · `CollapsibleTrigger.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'button'` | The element or component this component should render as. Can be overwritten by `asChild`. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-disabled` |  | Present while the part is disabled. |
| `data-state` | `'open' \| 'closed'` | The state the part is in, one value per state. |

## Styling

| Part | Recipe |
| --- | --- |
| `content` | [`collapsible-content`](https://ui.frontic.com/DESIGN.md#collapsible-content) |

## Source

`components/ui/collapsible/Collapsible.vue`

```vue
<script lang="ts">
/** Groups a trigger with content that can be shown or hidden. */
export default {}
</script>

<script setup lang="ts">
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'reka-ui'
import { CollapsibleRoot, useForwardPropsEmits } from 'reka-ui'

const props = defineProps<CollapsibleRootProps>()
const emits = defineEmits<CollapsibleRootEmits>()

defineSlots<{
  /** Holds the trigger and the content; gets whether the content is shown. */
  default?: (props: { open: boolean }) => unknown
}>()

const forwarded = useForwardPropsEmits(props, emits)
</script>

<template>
  <CollapsibleRoot v-slot="slotProps" data-slot="collapsible" v-bind="forwarded">
    <slot v-bind="slotProps" />
  </CollapsibleRoot>
</template>
```

`components/ui/collapsible/CollapsibleContent.vue`

```vue
<script lang="ts">
/** Holds what its collapsible shows while open. */
export default {}
</script>

<script setup lang="ts">
import type { CollapsibleContentProps } from 'reka-ui'
import { CollapsibleContent, useForwardProps } from 'reka-ui'

const props = defineProps<CollapsibleContentProps>()
const forwarded = useForwardProps(props)
</script>

<template>
  <CollapsibleContent data-slot="collapsible-content" v-bind="forwarded" class="fui-collapsible-content data-open:animate-collapsible-down data-closed:animate-collapsible-up overflow-hidden">
    <slot />
  </CollapsibleContent>
</template>
```

`components/ui/collapsible/CollapsibleTrigger.vue`

```vue
<script lang="ts">
/** Opens and closes the content of its collapsible. */
export default {}
</script>

<script setup lang="ts">
import type { CollapsibleTriggerProps } from 'reka-ui'
import { CollapsibleTrigger, useForwardProps } from 'reka-ui'

const props = defineProps<CollapsibleTriggerProps>()
const forwarded = useForwardProps(props)
</script>

<template>
  <CollapsibleTrigger data-slot="collapsible-trigger" v-bind="forwarded">
    <slot />
  </CollapsibleTrigger>
</template>
```

## API of the items the example uses

### Image

[Full docs](https://ui.frontic.com/components/ui/image.md) · `npx @frontic/ui add image`

#### Image

Shows an image with a loading state and a fallback if it fails to load. Renders a `<span>`.

`data-slot="image"` · `Image.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `src` | `string` |  | URL of the image to show. |
| `alt` | `string` |  | Text alternative read by screen readers; leave empty for a purely decorative image. |
| `loadingEffect` | `LoadingEffect`: `'skeleton' \| 'blur' \| 'none'` | `'skeleton'` | What shows until the image has loaded: a pulsing placeholder, the `placeholderSrc` preview blurred, or nothing. Blur without a `placeholderSrc` shows the pulsing placeholder. |
| `fallbackSrc` | `string` |  | URL tried when `src` fails to load; the fallback slot shows when both fail. |
| `placeholderSrc` | `string` |  | URL of a tiny version of the image, e.g. 24px wide, shown blurred while `src` loads when `loadingEffect` is `blur`. |
| `priority` | `boolean` | `false` | Loads the image first, eagerly and at high fetch priority, instead of when it nears the viewport. Set it on the one image that is the page's largest paint, such as a hero or the first gallery image. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `fallback` |  | Replaces the placeholder shown when the image fails to load and no fallback URL loads either. |

#### ImageFallback

Shows a placeholder in place of an image that failed to load. Renders a `<span>`.

`data-slot="image-fallback"` · `ImageFallback.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### Price

[Full docs](https://ui.frontic.com/components/ui/price.md) · `npx @frontic/ui add price`

#### Price

Shows a price, with the original price and discount percentage when reduced. Renders a `<span>`.

`data-slot="price"` · `Price.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `size` | `PriceSize`: `'md' \| 'sm' \| 'lg'` | `'md'` | The text size of the price. |
| `align` | `PriceAlign`: `'start' \| 'end'` | `'start'` | Which side the badge, the prices and the hint line up on. |
| `price` (required) | `PriceType` |  | The price to show: `amount` and `ref` in minor units such as cents, `currency` as an ISO 4217 code. |
| `locale` | `string` |  | The locale that formats the price, e.g. `de-DE`; the ConfigProvider's when left out. |
| `showRef` | `boolean` | `true` | Shows the reference price struck through when it is higher than the amount. |
| `showDiscount` | `boolean` | `true` | Shows the saving as a percentage next to a discounted price. |
| `showHint` | `boolean` | `true` | Shows the hint below the price when the hint slot holds one. |
| `refLabel` | `string` | `'Was'` | Read out before the original price of a reduced product; hidden on screen. |
| `amountLabel` | `string` | `'Now'` | Read out before the current price of a reduced product; hidden on screen. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `hint` |  | A note below the price, such as tax and shipping. |

#### PriceAmount

Shows the current price of the product. Renders a `<span>`.

`data-slot="price-amount"` · `PriceAmount.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

#### PriceRef

Shows the original price of a discounted product. Renders a `<span>`.

`data-slot="price-ref"` · `PriceRef.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

#### PriceDiscount

Shows the percentage saved against the original price. Renders a `<span>`.

`data-slot="price-discount"` · `PriceDiscount.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ percent: number; }` | Replaces the discount badge; gets the percentage saved. |

#### PriceHint

Holds a note below the price, such as tax and shipping, which may link on. Renders a `<span>`.

`data-slot="price-hint"` · `PriceHint.vue`

Styling: `price-hint`, which extends [`link-foreground`](https://ui.frontic.com/DESIGN.md#link-foreground). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'span'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
