Skip to content
omaris

Containment

Collapsible

A single disclosure: a header you press and the block it shows or hides.

import { Collapsible } from 'omaris'
Learn

Examples

Basic

One header, one body. bind:open holds the state; the body grows to its own height and the chevron turns with it.

Karada, Street 62, house 14 — ring twice.

The courier calls from 07xx when they are ten minutes away.

<script lang="ts">	import { Collapsible, Text } from 'omaris';​	let open = $state(false);</script>​<div class="w-full max-w-md">	<Collapsible		bind:open		variant="outlined"		title="Delivery details"		supportingText="Tiger Express · today, 2–6 pm"	>		<div class="flex flex-col gap-2">			<Text variant="body-md">Karada, Street 62, house 14 — ring twice.</Text>			<Text variant="body-sm" tone="muted">				The courier calls from 07xx when they are ten minutes away.			</Text>		</div>	</Collapsible></div>

States

A leading icon and a count in end for a panel that says how much it holds; filled when it is a thing of its own; disabled with the reason in the supporting line.

No refunds yet.

<script lang="ts">	import { Badge, Collapsible, Switch, Text } from 'omaris';​	let open = $state(true);	let settings = $state({ spicy: true, vegan: false, delivery: true });</script>​<div class="flex w-full max-w-md flex-col gap-4">	<Collapsible bind:open variant="filled" title="Filters">		{#snippet icon()}			...		{/snippet}		{#snippet end()}			<Badge size="sm">{Object.values(settings).filter(Boolean).length}</Badge>		{/snippet}		<div class="flex flex-col gap-3">			<Switch bind:checked={settings.spicy} label="Spicy dishes" />			<Switch bind:checked={settings.vegan} label="Vegan only" />			<Switch bind:checked={settings.delivery} label="Delivers now" />		</div>	</Collapsible>​	<Collapsible		variant="outlined"		disabled		title="Refund history"		supportingText="Available once the order is delivered"	>		<Text variant="body-md" tone="muted">No refunds yet.</Text>	</Collapsible></div>

Custom trigger

Any button can be the trigger: trigger hands you aria-expanded, aria-controls and the toggle to spread on it. Here, the "More options" under a form's one required field.

<script lang="ts">	import { Button, Collapsible, Input } from 'omaris';​	let open = $state(false);</script>​<div class="flex w-full max-w-sm flex-col gap-2">	<Input label="Restaurant name" value="Beit Baghdad" />​	<Collapsible bind:open classes={{ inner: 'flex flex-col gap-4 px-0 pt-2' }}>		{#snippet trigger(props)}			<Button variant="text" size="sm" {...props}>				{open ? 'Fewer options' : 'More options'}				<svg					viewBox="0 0 24 24"					fill="none"					stroke="currentColor"					stroke-width="2"					aria-hidden="true"					class={[						'transition-[rotate] duration-300 ease-emphasized motion-reduce:transition-none',						open && 'rotate-180'					]}				>					<path d="m6 9 6 6 6-6" stroke-linecap="round" stroke-linejoin="round" />				</svg>			</Button>		{/snippet}		<Input label="Slug" value="beit-baghdad" />		<Input label="WhatsApp number" value="0770 123 4567" />	</Collapsible></div>

Overridden

Every part is reachable: class for the root, classes for the trigger, chevron and body. headingLevel makes the trigger a real heading for a disclosure that titles a section.

Faster menus, Kurdish plurals, and a lighter spinner.
<script lang="ts">	import { Collapsible } from 'omaris';</script>​<div class="w-full max-w-md">	<Collapsible		open		headingLevel={3}		variant="outlined"		title="Release notes"		supportingText="v2.4.0"		class="rounded-shape-2xl border-primary"		classes={{			trigger: 'bg-primary-container text-primary-container-foreground',			supporting: 'text-primary-container-foreground/80',			chevron: 'text-primary-container-foreground',			inner: 'pt-4 font-mono text-xs'		}}	>		Faster menus, Kurdish plurals, and a lighter spinner.	</Collapsible></div>

When to use it

Use it for

  • One block of detail behind a header — the delivery details on an order, the raw payload under a log line, the refund history on a receipt.
  • "More options" under a form's required fields: the fields most people never touch, one press away. trigger takes your own Button for it.
  • A filter panel that folds away on a phone, with a count in end that says how much is applied while it is closed.
  • Open state that lives somewhere else: open is bindable, so it can come from the URL, from storage, or from a toggle elsewhere on the page.

Not for

  • A set of sections read one after another → Accordion, which keeps the headers in one list, moves between them with the arrows, and can close the others when one opens.
  • Peers where exactly one is visible → Tabs.
  • Content that should float over the page instead of pushing it down → Popover, or Sheet on a phone.
  • Sections of navigation → Navigation Drawer.

Do

  • Say what is inside in the header: "Delivery details", not "More". Use supportingText for the one fact worth reading without opening it.
  • Pick the surface to match what is around it: plain inside a card, outlined among other blocks, filled when it is a thing of its own.
  • Use disabled with the reason in supportingText rather than hiding a section that cannot open yet.
  • Set headingLevel when the disclosure titles a section of the page, so it shows up in a screen reader's list of headings.

Don't

  • Put a required field, or an error, in a closed body. A validation message nobody can see is a form nobody can submit.
  • Stack three collapsibles in a column; that is an Accordion.
  • Put a button or a link in title or end — the header is already a button.
  • Nest one inside another more than once. Two levels of folding is where people stop finding things.

Quick reference

variant
  • plain (default)
  • outlined
  • filled

API

Collapsible

A single disclosure: a header you press and the block it shows or hides.

The one-panel sibling of Accordion, with the same motion — the body rides a grid row going 0fr → 1fr, which is the one way to transition to a content-derived height without measuring it, so nothing jumps when the content changes while it is open. The chevron turns with it on the same curve, and the body fades in a beat behind so text never appears half clipped.

The header is either the built-in one — title, supportingText, an icon and an end slot, with the chevron — or your own element through trigger, which is handed the ARIA and the handler to spread on it:

A closed body is inert, so nothing inside it can take focus or be read out while it is out of sight.

import { Collapsible } from 'omaris'
<Collapsible bind:open>  {#snippet trigger(props)}    <Button variant="text" {...props}>{open ? 'Show less' : 'Show all 12'}</Button>  {/snippet}  …</Collapsible>

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
No description in the source yet.
trigger
No description in the source yet.
icon
Leading icon in the built-in trigger.
headline
No description in the source yet.
title
No description in the source yet.
supporting
No description in the source yet.
chevron
No description in the source yet.
wrapper
The animated wrapper; its single grid row is what opens.
content
Clips the body while the row is shorter than it.
inner
No description in the source yet.

Props

open bindable

Defaults to false

boolean

Whether the body is showing. Bindable.

onOpenChange
(open: boolean) => void

Called whenever it opens or closes from the trigger.

title
string

Header text for the built-in trigger.

supportingText
string

A second line under title — "3 filters applied".

variant

Defaults to 'plain'

CollapsibleVariant

plain flush on the page, outlined a hairline card, filled a tonal one.

plain
Flush on the page — no surface of its own.
outlined
A hairline card, for a disclosure that sits among other things.
filled
A tonal card, for a disclosure that is a thing of its own.
disabled

Defaults to false

boolean

The trigger does nothing and reads as unavailable. The body keeps its state.

headingLevel
2 | 3 | 4 | 5 | 6

Wraps the built-in trigger in a heading of this level, for a disclosure that titles a section of the page. Leave unset for one that doesn't.

class
string
classes
CollapsibleClasses

Per-part Tailwind overrides. class still covers the root.

icon
Snippet

Leading icon in the built-in trigger.

end
Snippet

Trailing content before the chevron — a count, a badge.

trigger
Snippet<[CollapsibleTriggerProps]>

Your own trigger in place of the built-in one. Spread the props onto a button — they carry aria-expanded, aria-controls and the toggle.

children
Snippet

The body.

id required
string
aria-expanded required
boolean
aria-controls required
string
onclick required
(event: MouseEvent) => void