Collapsible

A region a single trigger opens and closes.

On my first day at the new school I met a girl on the roof holding an old camera. She said it could photograph tomorrow.

<script setup lang="ts">
  import {
    Button,
    Card,
    Collapsible,
    CollapsibleContent,
    CollapsibleTrigger,
    DisclosureIcon,
    Stack,
    Text,
  } from '@hina-ui/vue'
</script>

<template>
  <Card class="w-full max-w-md">
    <Collapsible>
      <Stack gap="sm" align="start">
        <Text tone="muted" size="sm">
          On my first day at the new school I met a girl on the roof holding an old camera. She said
          it could photograph tomorrow.
        </Text>
        <CollapsibleTrigger as-child>
          <Button variant="link" size="sm">
            <template #trailing>
              <DisclosureIcon />
            </template>
            Read the full synopsis
          </Button>
        </CollapsibleTrigger>
        <CollapsibleContent>
          <Text tone="muted" size="sm">
            Every day after that we climbed the rusted ladder once class let out. She pressed the
            shutter and I kept the time. Then one evening something appeared in the viewfinder that
            should not have been there.
          </Text>
        </CollapsibleContent>
      </Stack>
    </Collapsible>
  </Card>
</template>
vue

Usage

import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@hina-ui/vue'
ts

Three parts: Collapsible holds the open state, CollapsibleTrigger is the control that toggles it, and CollapsibleContent is the region being folded away.

The trigger is a complete control on its own and brings an indicator that turns with the state. Write the label and nothing else — no button to compose, no transition to handle.

<script setup lang="ts">
  import { Collapsible, CollapsibleContent, CollapsibleTrigger, Stack, Text } from '@hina-ui/vue'
</script>

<template>
  <Collapsible class="w-full max-w-md">
    <Stack gap="sm" align="start">
      <CollapsibleTrigger>Reading settings</CollapsibleTrigger>
      <CollapsibleContent>
        <Text tone="muted" size="sm">
          Type size, line height, page direction and background colour all live here.
        </Text>
      </CollapsibleContent>
    </Stack>
  </Collapsible>
</template>
vue

Its appearance is fixed at one unobtrusive step, since open and closed are the only two things it has to say. Where the label belongs in the text — a “read the rest” inside a paragraph — swap the skin through as-child, below.

Use it when a single region folds away. For several regions in one group that open one at a time, use Accordion.

Examples

Controlled

v-model:open hands the state to the surrounding page, so other controls can drive the same region. When no outside control is needed, default-open sets the initial state.

open: false

<script setup lang="ts">
  import { ref } from 'vue'
  import {
    Button,
    Collapsible,
    CollapsibleContent,
    CollapsibleTrigger,
    Inline,
    Stack,
    Text,
  } from '@hina-ui/vue'

  const open = ref(false)
</script>

<template>
  <Stack class="w-full max-w-md">
    <Inline>
      <Button size="sm" variant="soft" tone="neutral" @click="open = !open">
        {{ open ? 'Collapse' : 'Expand' }}
      </Button>
      <Text tone="muted" size="sm">open: {{ open }}</Text>
    </Inline>
    <Collapsible v-model:open="open">
      <Stack gap="sm" align="start">
        <CollapsibleTrigger>The component's own trigger</CollapsibleTrigger>
        <CollapsibleContent>
          <Text tone="muted" size="sm">Both triggers drive the same state.</Text>
        </CollapsibleContent>
      </Stack>
    </Collapsible>
  </Stack>
</template>
vue

Disabled

With disabled the trigger stops responding and the content keeps its current state.

<script setup lang="ts">
  import { Collapsible, CollapsibleContent, CollapsibleTrigger, Stack, Text } from '@hina-ui/vue'
</script>

<template>
  <Collapsible disabled class="w-full max-w-md">
    <Stack gap="sm" align="start">
      <CollapsibleTrigger>Edit history (sign in required)</CollapsibleTrigger>
      <CollapsibleContent>
        <Text tone="muted" size="sm">This section will not open.</Text>
      </CollapsibleContent>
    </Stack>
  </Collapsible>
</template>
vue

Another glyph, or your own trigger

The icon slot swaps the indicator’s glyph while the component keeps owning the rotation. Setting icon to false hides the indicator entirely.

as-child lends the behaviour to the sole child, which puts both the appearance and the indicator in your hands. Every change of skin — a text link, an outline, a full-width row with an icon slot — goes this way; drop in a DisclosureIcon and it still finds the state. The example at the top of this page is exactly that, with variant="link".

The component forwards no appearance props: a disclosure toggle only has to express open and closed, and taking on a button’s full range of looks would grow this API into a second Button. Changing the skin means bringing the whole trigger.

<script setup lang="ts">
  import { Plus } from '@lucide/vue'
  import {
    Button,
    Collapsible,
    CollapsibleContent,
    CollapsibleTrigger,
    DisclosureIcon,
    Stack,
    Text,
  } from '@hina-ui/vue'
</script>

<template>
  <Stack class="w-full max-w-md">
    <Collapsible>
      <Stack gap="sm" align="start">
        <CollapsibleTrigger>
          Swap the glyph
          <template #icon>
            <Plus />
          </template>
        </CollapsibleTrigger>
        <CollapsibleContent>
          <Text tone="muted" size="sm">
            A plus turns a quarter into a cross, and the component still owns the rotation.
          </Text>
        </CollapsibleContent>
      </Stack>
    </Collapsible>
    <Collapsible>
      <Stack gap="sm" align="start">
        <CollapsibleTrigger as-child>
          <Button variant="outline" tone="neutral" block class="justify-between">
            Bring your own trigger
            <template #trailing>
              <DisclosureIcon />
            </template>
          </Button>
        </CollapsibleTrigger>
        <CollapsibleContent>
          <Text tone="muted" size="sm">
            On this path the appearance and the indicator are both yours — place a DisclosureIcon
            and it still finds the state.
          </Text>
        </CollapsibleContent>
      </Stack>
    </Collapsible>
  </Stack>
</template>
vue

Behaviour

  • The content’s height animates as it opens and closes, both directions on the same step.
  • While closed the content leaves the accessibility tree, so the keyboard never lands inside it.

API

Collapsible

Prop
Type
Default
Description
open
boolean
—
Whether it is open, takes v-model:open
defaultOpen
boolean
false
Whether it starts open
disabled
boolean
false
Whether it can be operated
class
string
—
Classes appended to the root
Event
Payload
Description
update:open
open: boolean
The state changed

CollapsibleTrigger

Prop
Type
Default
Description
icon
boolean
true
Whether the indicator is shown
asChild
boolean
false
Skip the built-in button and lend behaviour to the child
class
string
—
Classes appended to the trigger
Slot
Description
default
The trigger’s label
icon
Replaces the indicator’s glyph

CollapsibleContent

Prop
Type
Default
Description
class
string
—
Classes appended to the content